Table of Contents
KEY TAKEAWAYS
- SRP means one module = one reason to change
- Split responsibilities by asking: “If I change X, should Y also need to change?”
- SRP makes code easier to test, reuse, and maintain
- The orchestration (combining responsibilities) happens in a higher-level function like
main
What Is the Single Responsibility Principle?
The Single Responsibility Principle (SRP) states: a module (or function) should have only one reason to change. In other words, every function, file, or module should do one thing and do it well.When a module has multiple responsibilities, changing one responsibility risks breaking the other. SRP keeps your code focused, testable, and easy to maintain.Example 1: Sensor Module That Does Too Much
Bad: One module reads sensor AND logs AND checks alarms
/* sensor.c - does three different jobs */
#include <stdio.h>
#include "uart.h"
#define TEMP_THRESHOLD 80
static int last_reading = 0;
int sensor_read_and_process(void) {
/* Responsibility 1: Read hardware */
int raw = ADC_REG;
int temperature = (raw * 330) / 1024;
last_reading = temperature;
/* Responsibility 2: Log to UART */
char buf[32];
sprintf(buf, "TEMP: %d Crn", temperature);
uart_send_string(buf);
/* Responsibility 3: Check alarm */
if (temperature > TEMP_THRESHOLD) {
GPIO_SET(BUZZER_PIN);
uart_send_string("ALARM: Overheating!rn");
} else {
GPIO_CLEAR(BUZZER_PIN);
}
return temperature;
}Problems:- Cannot test the temperature conversion without real hardware and UART
- Changing the log format risks breaking the alarm logic
- Changing the alarm threshold requires editing the sensor file
Good: Split into three focused modules
/* sensor.c - only reads and converts temperature */
int sensor_read_temperature(void) {
int raw = ADC_REG;
return (raw * 330) / 1024;
}
/* logger.c - only handles logging */
void logger_log_temperature(int temperature) {
char buf[32];
sprintf(buf, "TEMP: %d Crn", temperature);
uart_send_string(buf);
}
/* alarm.c - only handles alarm logic */
#define TEMP_THRESHOLD 80
void alarm_check_temperature(int temperature) {
if (temperature > TEMP_THRESHOLD) {
GPIO_SET(BUZZER_PIN);
logger_log_temperature(temperature); /* Optional */
} else {
GPIO_CLEAR(BUZZER_PIN);
}
}
/* main.c - orchestrates */
void main_loop(void) {
int temp = sensor_read_temperature();
logger_log_temperature(temp);
alarm_check_temperature(temp);
}Benefits: Each file has one reason to change. You can test the alarm logic by passing fake temperature values. Changing the log format does not touch sensor code.Example 2: A Function That Parses, Validates, AND Stores
Bad: One function doing everything
/* Parses a "KEY=VALUE" config line, validates it, and stores it */
int process_config_line(const char *line) {
/* Parse */
char key[32], value[32];
if (sscanf(line, "%31[^=]=%31s", key, value) != 2) {
printf("Parse error: %s\n", line);
return -1;
}
/* Validate */
if (strlen(key) == 0 || strlen(value) == 0) {
printf("Empty key or valuen");
return -1;
}
if (strlen(key) > 16) {
printf("Key too long: %s\n", key);
return -1;
}
/* Store */
for (int i = 0; i < config_count; i++) {
if (strcmp(config[i].key, key) == 0) {
strcpy(config[i].value, value);
return 0; /* Updated existing */
}
}
strcpy(config[config_count].key, key);
strcpy(config[config_count].value, value);
config_count++;
return 1; /* Added new */
}Good: Three separate functions
/* parse.c */
int parse_key_value(const char *line, char *key, int key_max,
char *value, int val_max) {
return sscanf(line, "%[^=]=%s", key, value) == 2 ? 0 : -1;
}
/* validate.c */
int validate_key_value(const char *key, const char *value) {
if (key == NULL || value == NULL) return -1;
if (strlen(key) == 0 || strlen(value) == 0) return -1;
if (strlen(key) > 16) return -1;
return 0;
}
/* config_store.c */
int config_store(const char *key, const char *value) {
for (int i = 0; i < config_count; i++) {
if (strcmp(config[i].key, key) == 0) {
strcpy(config[i].value, value);
return 0;
}
}
strcpy(config[config_count].key, key);
strcpy(config[config_count].value, value);
config_count++;
return 1;
}
/* main.c - orchestrates */
int process_config_line(const char *line) {
char key[32], value[32];
if (parse_key_value(line, key, 32, value, 32) != 0) return -1;
if (validate_key_value(key, value) != 0) return -1;
return config_store(key, value);
}Now each function can be tested independently. validate_key_value can be unit tested with dozens of edge cases without needing a real config file.When to Apply SRP
- When a function is longer than ~30 lines, look for multiple responsibilities
- When a file has #include for unrelated headers (e.g., sensor.h including uart.h and buzzer.h)
- When changing one feature breaks another unrelated feature
When NOT to Over-Apply
- Do not create a separate file for every 5-line function — that creates file bloat
- Simple programs with one clear flow do not need to be split into 10 modules
- Use your judgment: if two things always change together, they might belong together
Key Takeaways
- SRP means one module = one reason to change
- Split responsibilities by asking: “If I change X, should Y also need to change?”
- SRP makes code easier to test, reuse, and maintain
- The orchestration (combining responsibilities) happens in a higher-level function like
main
Example 3: Report Generator Doing Too Much
A report generation function that gathers data, formats output, and sends it over a network violates SRP by handling three distinct responsibilities.Bad — One Function, Three Jobs
void generate_and_send_report(void) {
// 1. Gather data
float temps[10];
for (int i = 0; i < 10; i++) {
temps[i] = read_sensor(i);
}
// 2. Format report
char report[1024];
int offset = 0;
offset += sprintf(report + offset, "=== Sensor Report ===\n");
for (int i = 0; i < 10; i++) {
offset += sprintf(report + offset, "Sensor %d: %.1f Cn", i, temps[i]);
}
// 3. Send over UART
uart_init(9600);
uart_send(report, offset);
uart_flush();
}If you change the report format, you touch the same function that handles networking. If UART is replaced with SPI, you edit the function that gathers sensor data. Every change risks breaking unrelated functionality.Good — Three Functions, Three Jobs
// Data collection
void gather_sensor_data(float *temps, int count) {
for (int i = 0; i < count; i++) {
temps[i] = read_sensor(i);
}
}
// Formatting
int format_report(const float *temps, int count, char *buf, int buf_size) {
int offset = 0;
offset += snprintf(buf + offset, buf_size - offset, "=== Sensor Report ===\n");
for (int i = 0; i < count; i++) {
offset += snprintf(buf + offset, buf_size - offset, "Sensor %d: %.1f Cn", i, temps[i]);
}
return offset;
}
// Transmission
void send_report(const char *data, int length) {
uart_init(9600);
uart_send(data, length);
uart_flush();
}
// Orchestration
void report_cycle(void) {
float temps[10];
char report[1024];
gather_sensor_data(temps, 10);
int len = format_report(temps, 10, report, sizeof(report));
send_report(report, len);
}Now each function has one reason to change. You can test format_report without hardware, swap UART for SPI in send_report without touching the formatter, and reuse gather_sensor_data in a different context entirely.How to Identify SRP Violations
Look for these warning signs:- Functions longer than 30-40 lines — often doing multiple things
- Multiple “sections” separated by comments — each section is a candidate for its own function
- A function name containing “and” —
parse_and_validate,read_and_process,format_and_send - Hard to write a unit test — if testing one aspect requires setting up unrelated dependencies, the function has too many responsibilities

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.







