Table of Contents
KEY TAKEAWAYS
- The Factory pattern centralizes object creation behind a common function
- Callers use a uniform interface without knowing the concrete implementation
- In C, factories return struct pointers with function pointers as the “interface”
- Combine with the Strategy pattern for maximum flexibility
What Is the Factory Pattern?
The Factory pattern creates objects (or structs, in C) through a common interface without exposing the creation logic to the caller. Instead of the caller knowing how to construct every specific type, a factory function handles the details and returns a ready-to-use instance.In C, the Factory pattern typically means: a function that takes a type identifier (enum, string, or integer) and returns an initialized struct or interface pointer. This centralizes construction logic and hides implementation details.Example 1: Communication Interface Factory
Bad — Caller Builds Each Type Manually
void app_init(int comm_type) {
if (comm_type == COMM_UART) {
uart_init(9600, 8, 1);
uart_set_flow_control(FLOW_NONE);
// 5 more lines of UART setup...
} else if (comm_type == COMM_SPI) {
spi_init(SPI_MODE0, 1000000);
spi_set_cs_pin(GPIO_PIN_4);
// 5 more lines of SPI setup...
} else if (comm_type == COMM_I2C) {
i2c_init(100000);
i2c_set_address(0x50);
// 5 more lines of I2C setup...
}
}The application code is buried in hardware-specific initialization details. Every new comm type means modifying the caller.Good — Factory Function
// Common interface
typedef struct {
int (*send)(const uint8_t *data, int len);
int (*recv)(uint8_t *buf, int max_len);
void (*close)(void);
} CommInterface;
// Factory function
CommInterface *comm_create(CommType type) {
switch (type) {
case COMM_UART: return uart_create_interface();
case COMM_SPI: return spi_create_interface();
case COMM_I2C: return i2c_create_interface();
default: return NULL;
}
}
// Each module handles its own setup internally
CommInterface *uart_create_interface(void) {
static CommInterface iface;
uart_init(9600, 8, 1);
uart_set_flow_control(FLOW_NONE);
iface.send = uart_send;
iface.recv = uart_recv;
iface.close = uart_close;
return &iface;
}
// Application code — clean and simple
void app_init(CommType type) {
CommInterface *comm = comm_create(type);
comm->send(hello_msg, sizeof(hello_msg));
}The caller does not know or care whether it is using UART, SPI, or I2C. The factory handles all construction details. Adding a new comm type means writing one xxx_create_interface() function and one case in the factory.Example 2: Logger Factory
Bad — Scattered Logger Creation
void setup_logging(const char *target) {
if (strcmp(target, "uart") == 0) {
uart_init(115200, 8, 1);
// global logger state for UART
g_log_func = uart_log;
} else if (strcmp(target, "file") == 0) {
g_log_file = fopen("/var/log/app.log", "a");
g_log_func = file_log;
} else if (strcmp(target, "syslog") == 0) {
openlog("myapp", LOG_PID, LOG_USER);
g_log_func = syslog_log;
}
}Good — Logger Factory with Consistent Interface
typedef struct Logger {
void (*log)(struct Logger *self, const char *level, const char *msg);
void (*destroy)(struct Logger *self);
void *ctx; // backend-specific data
} Logger;
// UART logger
static void uart_log(Logger *self, const char *level, const char *msg) {
char buf[256];
int len = snprintf(buf, sizeof(buf), "[%s] %srn", level, msg);
uart_send((uint8_t *)buf, len);
}
Logger *logger_create_uart(int baud) {
Logger *l = malloc(sizeof(Logger));
uart_init(baud, 8, 1);
l->log = uart_log;
l->destroy = uart_logger_destroy;
l->ctx = NULL;
return l;
}
// File logger
static void file_log(Logger *self, const char *level, const char *msg) {
FILE *f = (FILE *)self->ctx;
fprintf(f, "[%s] %s\n", level, msg);
fflush(f);
}
Logger *logger_create_file(const char *path) {
Logger *l = malloc(sizeof(Logger));
l->ctx = fopen(path, "a");
l->log = file_log;
l->destroy = file_logger_destroy;
return l;
}
// Factory
Logger *logger_create(const char *type) {
if (strcmp(type, "uart") == 0) return logger_create_uart(115200);
if (strcmp(type, "file") == 0) return logger_create_file("/var/log/app.log");
return NULL;
}
// Usage — backend-agnostic
Logger *log = logger_create("file");
log->log(log, "INFO", "System started");
log->log(log, "ERROR", "Sensor timeout");
log->destroy(log);The application code uses the Logger interface without knowing the backend. Switching from file logging to UART logging means changing one string. Each backend manages its own resources internally.Factory Pattern Variations
- Simple Factory — a function with a switch/if that returns the right type (shown above)
- Registration Factory — modules register their factory functions at startup, and the factory looks them up by name. This combines Factory with Open/Closed Principle
- Abstract Factory — a struct of factory function pointers, letting you swap entire families of related objects. Common in HAL (Hardware Abstraction Layer) design
When to Use the Factory Pattern
- Multiple implementations of the same interface (UART vs SPI vs I2C)
- Complex initialization that should be hidden from the caller
- Runtime selection — choosing which implementation based on config, hardware detection, or user input
- Testing — the factory can return mock implementations during unit tests
Key Takeaways
- The Factory pattern centralizes object creation behind a common function
- Callers use a uniform interface without knowing the concrete implementation
- In C, factories return struct pointers with function pointers as the “interface”
- Combine with the Strategy pattern for maximum flexibility

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







