What’s the Command Pattern, Anyway?

The Command pattern is a behavioral design pattern that packages an action into an object, turning the operation itself into a first-class citizen of the system. Instead of calling a function directly, the action is represented as a command object that stores everything needed to run it later: the target method, the receiver that owns the method, and any parameters required for execution.

A typical Command setup involves four roles: command, receiver, invoker, and client. The command object holds a reference to the receiver and implements an execute() method that simply forwards the call to the receiver. The invoker is responsible for triggering commands and only knows the command interface, not the concrete operation. The client wires everything together by choosing which receiver each command should use and deciding when commands should be executed.

The diagram below closely follows the structure presented in the GoF «Design Patterns» book:

command pattern generic

Notice how the invoker is entirely agnostic to the concrete command's logic, holding only a reference to the command interface.

In C-based embedded systems, the Command pattern behaves much like a callback on steroids. Instead of passing a bare function pointer, a command object bundles the function pointer together with its execution context, parameters, and the target subsystem (the receiver).

This turns the operation itself into a first-class citizen that can be queued and deferred, thus allowing an asynchronous execution. This in turn allows the ISR to simply capture the event and enqueue the corresponding command without performing any heavy processing inside the interrupt.

 


A minimal C based approach

The classic use case of the Command pattern in embedded systems is a keypad that triggers different actions depending on which key is pressed. Instead of hard‑coding logic inside the keypad module, each key is associated with a command object that encapsulates the action to be performed.

This offers a clean separation of responsibilities:

A major benefit of this approach is that the keypad becomes reprogrammable: each key can be dynamically assigned a different command at runtime or during system initialization, without modifying the keypad module itself.

command pattern minimal

 

cmd.h (command interface)

            
        #ifndef CMD_H
        #define CMD_H

        #include <stdint.h>

        //Command interface
        typedef struct cmd_t{
            void (*execute)(struct cmd_t *self);
        } cmd_t;

        #endif
            
        

 

cmd_led.h (concrete command header)

            
        #ifndef CMD_LED_H
        #define CMD_LED_H
        #include "cmd.h"
        #include "gpio_api.h"

        typedef struct {
            cmd_t base;
            gpio_t led_id;
        } cmd_led_t;

        cmd_t* CMD_LED_Init(cmd_led_t* self, gpio_t led_id);

        #endif
        

cmd_led.c (concrete command implementation)

            
        #include <stdio.h>
        #include <string.h>
        #include <assert.h>
        #include "cmd_led.h"

        static void CMD_LED_Toggle(cmd_t *self) {
            cmd_led_t *cmd = (cmd_led_t *)self;

            if (GPIO_ReadPin(cmd->led_id)){
                GPIO_SetPinL(cmd->led_id);
            } else {
                GPIO_SetPinH(cmd->led_id);
            }
        }

        cmd_t* CMD_LED_Init(cmd_led_t* self, gpio_t led_id){
            
            assert(self != NULL); 
            if (self == NULL) return NULL;

            self->led_id = led_id;
            self->base.execute = CMD_LED_Toggle;
            return (cmd_t*)self;
        }

        

 

gpio_api.h

            
            #ifndef GPIO_API_H
            #define GPIO_API_H

            #include <stdint.h>
            #include <stdbool.h>

            typedef enum {
                GPIO_LED1 = 0,
                GPIO_LED2 = 1,
                GPIO_LED3 = 2,
                GPIO_LED4 = 3,
                GPIO_COUNT
            } gpio_t;

            void GPIO_SetPinL(gpio_t gpio);
            void GPIO_SetPinH(gpio_t gpio);
            bool GPIO_ReadPin(gpio_t gpio);

            #endif

        

 

gpio_api.c

            
        #include <stdio.h>
        #include <stdbool.h>

        #include "gpio_api.h"

        static const char* mock_pin_map_table[GPIO_COUNT] = {
            [GPIO_LED1]   = "LED1_PIN",
            [GPIO_LED2]   = "LED2_PIN",
            [GPIO_LED3]   = "LED3_PIN",
            [GPIO_LED4]   = "LED4_PIN",
        };

        static bool mock_state[GPIO_COUNT] = {0};

        void GPIO_SetPinH(gpio_t gpio) {
            if (gpio < GPIO_COUNT) {
                printf("Setting pin %s HIGH\n", mock_pin_map_table[gpio]);
                mock_state[gpio] = true;
            } else {
                printf("Invalid GPIO ID: %d\n", gpio);
            }
        }

        void GPIO_SetPinL(gpio_t gpio) {
            if (gpio < GPIO_COUNT) {
                printf("Setting pin %s LOW\n", mock_pin_map_table[gpio]);
                mock_state[gpio] = false;
            } else {
                printf("Invalid GPIO ID: %d\n", gpio);
            }   
        }

        bool GPIO_ReadPin(gpio_t gpio) {
            return mock_state[gpio];
        }
        

 

invoker_keypad.h

            
        #ifndef INVOKER_KEYPAD_H
        #define INVOKER_KEYPAD_H

        #include "cmd.h"

        #define KEYPAD_KEYS 4

        typedef enum {
            KEYPAD_KEY_1 = 0,
            KEYPAD_KEY_2,
            KEYPAD_KEY_3,
            KEYPAD_KEY_4
        } keypad_key_t;

        void KEYPAD_Init(void);
        void KEYPAD_SetCommand(keypad_key_t key, cmd_t* cmd);
        void KEYPAD_KeyPress(keypad_key_t key);

        #endif
        

 

invoker_keypad.c

            
        #include "invoker_keypad.h"
        #include "queue.h"
        #include <assert.h>
        #include <stdio.h>
        #include <string.h>

        static cmd_t* commands[KEYPAD_KEYS];

        void KEYPAD_Init(void) {
            for (int i = 0; i < KEYPAD_KEYS; i++) {
                commands[i] = NULL;
            }
        }

        void KEYPAD_SetCommand(keypad_key_t key, cmd_t* cmd) {
            assert(key < KEYPAD_KEYS);
            commands[key] = cmd;
        }


        void KEYPAD_KeyPress(keypad_key_t key) {
            assert(key < KEYPAD_KEYS);

            const cmd_t* cmd = commands[key];
            if (cmd != NULL) {
                cmd->execute((cmd_t*)cmd);
            }
        }

        

        

 

main.c (client)

            
        #include "cmd_led.h"
        #include "invoker_keypad.h"

        //static memory allocation for command objects
        static cmd_led_t g_cmd_led1;
        static cmd_led_t g_cmd_led2;

        int main(void) {
            KEYPAD_Init();
            
            // Register commands to keys
            KEYPAD_SetCommand(KEYPAD_KEY_1, CMD_LED_Init(&g_cmd_led1, GPIO_LED1));
            KEYPAD_SetCommand(KEYPAD_KEY_2, CMD_LED_Init(&g_cmd_led2, GPIO_LED2));
            
            // Simulate key presses
            KEYPAD_KeyPress(KEYPAD_KEY_1);
            KEYPAD_KeyPress(KEYPAD_KEY_1);
            KEYPAD_KeyPress(KEYPAD_KEY_2);
            
            
            return 0;
        }
            
        

 

Output:

Setting pin LED1_PIN HIGH

Setting pin LED1_PIN LOW

Setting pin LED2_PIN HIGH