What’s the Object Pool Pattern, Anyway?

The Object Pool Pattern is a creational design pattern used to manage and reuse a fixed number of pre-allocated objects. Instead of creating and destroying objects on the fly, the pattern keeps a pool of ready-to-use pre-initialized instances. When a client requests an object, the pool hands out an available one. When the client is done, the object is returned to the pool.


Why Use the Object Pool Pattern in Embedded Systems?

The primary advantage of the object pool pattern is that it eliminates runtime dynamic memory allocation. Instead of repeatedly calling malloc() and free(), the system performs a single allocation during initialization and then reuses pre allocated objects. This approach prevents heap fragmentation and ensures deterministic, constant time allocation and deallocation.

Additionally, pool allocation and deallocation are guaranteed O(1) operations, surpassing traditional malloc, which provides no time-complexity guarantees. The runtime behavior of malloc is non-deterministic and implementation-dependent.

Furthermore, because each pool object is pre-initialized during the pool’s creation, the system avoids the extra runtime cost of initializing objects on the fly. This leads to more predictable timing and reduced latency during operation.


Object Pool Structure

pool structure

The pool is implemented as a contiguous block of memory containing a sequence of fixed-size pool items. At the beginning of this block, a small metadata table stores all information required to operate the pool. Following the metadata, the pool consists of a linked list of pool items.

Each item is composed of two parts: a lightweight header and an object payload. The header holds a pointer to the next free item, enabling the construction of the singly linked list. The payload (obj) contains the actual user‑defined object data.

The UML diagram below summarizes the architecture of the object-pool system:

pool uml

The pool_t structure maintains all metadata required to operate the pool:

You may have already noticed that I did not supply a POOL_Destroy function. The idea is that the pool’s lifetime is intentionally tied to the lifetime of the system. The pool performs a single memory allocation during initialization and never touches the heap again.

 


C implementation

pool.h

            
        #ifndef POOL_H
        #define POOL_H
        #include <stdint.h>

        typedef struct pool_item_t pool_item_t;
        typedef struct pool_t pool_t;

        struct pool_item_t {
            struct pool_item_t *next;
            uint8_t obj[];
        }__attribute__((aligned(8)));

        struct pool_t {
            pool_item_t *free_item;
            size_t item_count;
            size_t item_size;
            void (*init_fn)(void *obj);
            uint8_t items[];
        }__attribute__((aligned(8)));

        pool_t* POOL_Create(size_t item_count, size_t item_size, void (*init_fn)(void *obj));
        void *POOL_Alloc(pool_t *pool);
        void POOL_Free(pool_t *pool, void *obj);

        #endif
        

pool.c

            
        #include <stdint.h>
        #include <stddef.h>
        #include <stdlib.h>
        #include <string.h>
        #include "pool.h"

        static void POOL_Init(pool_t *pool){
            if (pool == NULL) {
                return;
            }

            size_t item_count = pool->item_count;

            for (size_t i = 0; i < item_count; i++) {
                pool_item_t *item = (pool_item_t *)((uintptr_t)pool->items + i * pool->item_size);

                if (i + 1 < item_count){
                item->next = (pool_item_t *)((uintptr_t)pool->items + (i + 1) * pool->item_size);
                } else{
                    item->next = NULL;
                }

                if (pool->init_fn) pool->init_fn(item->obj);
            }

        }


        pool_t* POOL_Create(size_t item_count, size_t item_size, void (*init_fn)(void *obj)){
            size_t pool_item_size = sizeof(pool_item_t) + item_size;
            size_t pool_total_size = sizeof(pool_t) + item_count * pool_item_size;

            pool_t *pool = malloc(pool_total_size);

            if (pool == NULL) return NULL;

            pool->item_count = item_count;
            pool->item_size = pool_item_size;
            pool->init_fn = init_fn;
            pool->free_item = (pool_item_t *)((uintptr_t)pool->items); //(pool_item_t *)(pool + 1);

            POOL_Init(pool);
            return pool;
        }



        void *POOL_Alloc(pool_t *pool){
            if (pool == NULL || pool->free_item == NULL){
                return NULL; // out of free items
            }

            pool_item_t *item = pool->free_item;    
            pool->free_item = item->next;

            return item->obj;
        }

        void POOL_Free(pool_t *pool, void *obj){
            if (pool == NULL || obj == NULL){
                return;
            }

            pool_item_t *item = (pool_item_t *)((uintptr_t)obj - offsetof(pool_item_t, obj));
            item->next = pool->free_item;
            pool->free_item = item;
        }

        

 

To demonstrate how the pool-object pattern operates in practice, we will integrate it to the command-pattern example we discussed here. Below are the necessary modifications:

cmd_led.h

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

        typedef struct {
            cmd_factory_t base;
            gpio_t led_id;
        } cmd_factory_led_t;

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

        cmd_factory_t* CMD_LED_FactoryInit(cmd_factory_led_t* cmd_factory_led, gpio_t led_id);
        void CMD_LED_SetPool(pool_t* pool_ptr);

        #endif

        

 

cmd_led.c

            
        #include <stdio.h>
        #include <string.h>
        #include <assert.h>
        #include <stdlib.h>
        #include <stddef.h>
        #include "cmd_led.h"
        #include "pool.h"

        static pool_t* pool;

        static void CMD_LED_Toggle(cmd_t *self) {
            cmd_led_t *cmd = (cmd_led_t *)self;
            
            printf("Time Stamp: %ld - ",cmd->timestamp);
            if (GPIO_ReadPin(cmd->led_id)){
                GPIO_SetPinL(cmd->led_id);
            } else {
                GPIO_SetPinH(cmd->led_id);
            }
        }

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

            cmd_led->led_id = led_id;
            cmd_led->base.execute = CMD_LED_Toggle;
                
            cmd_led->timestamp = rand(); // Simulating a different time stamp per call
            return (cmd_t*)cmd_led;
        }

        static cmd_t* CMD_LED_FactoryCreate(cmd_factory_t *self) {
            cmd_factory_led_t *factory = (cmd_factory_led_t*)self;

            cmd_led_t *cmd = POOL_Alloc(pool);
            return CMD_LED_Init(cmd, factory->led_id);
        }

        static void CMD_LED_FactoryDestroy(cmd_t *cmd) {
            cmd_led_t *led_cmd = (cmd_led_t*)cmd;
            
            POOL_Free(pool, led_cmd);
        }

        cmd_factory_t* CMD_LED_FactoryInit(cmd_factory_led_t* cmd_factory_led, gpio_t led_id) {
            cmd_factory_led->led_id = led_id;
            cmd_factory_led->base.create  = CMD_LED_FactoryCreate;
            cmd_factory_led->base.destroy = CMD_LED_FactoryDestroy;
            return (cmd_factory_t*)cmd_factory_led;
        }

        void CMD_LED_SetPool(pool_t * pool_ptr){
            pool = pool_ptr;
        }       

        

 

main.c (client)

            
        #include <time.h>
        #include <stdlib.h>
        #include <stdio.h>

        #include "cmd_led.h"
        #include "executor.h"
        #include "invoker_keypad.h"
        #include "circular_buffer.h"
        #include "pool.h"

        int main(void) {
            // Stack-allocated command factory objects
            cmd_factory_led_t g_cmd_factory_led1;
            cmd_factory_led_t g_cmd_factory_led2;

            // Array of cmd_bundle_t
            cmd_bundle_t cmd_bundle[10] = {NULL};   
            circular_buffer_t command_buffer;
            
            srand(time(NULL));

            CIRCULAR_BUFFER_Init(&command_buffer, cmd_bundle, sizeof(cmd_bundle)/sizeof(cmd_bundle[0]), sizeof(cmd_bundle[0]));
                            
            KEYPAD_Init(&command_buffer);
            
            // Allocate 10 items
            pool_t* cmd_led_pool = POOL_Create(10, sizeof(cmd_led_t),NULL);
            CMD_LED_SetPool(cmd_led_pool);

            KEYPAD_SetCommand(KEYPAD_KEY_1, CMD_LED_FactoryInit(&g_cmd_factory_led1,GPIO_LED1));
            KEYPAD_SetCommand(KEYPAD_KEY_2, CMD_LED_FactoryInit(&g_cmd_factory_led2,GPIO_LED2));

            KEYPAD_KeyPress(KEYPAD_KEY_1);
            KEYPAD_KeyPress(KEYPAD_KEY_2);
            KEYPAD_KeyPress(KEYPAD_KEY_2);
            KEYPAD_KeyPress(KEYPAD_KEY_1);

            while (!EXECUTOR_Execute(EXECUTOR_Dequeue(&command_buffer)));

            return 0;
        }