How To Write Modular Code and Why You Should
2026-02-25 | By Maker.io Staff

Unstructured code makes programs harder to maintain and debug. Even though modern languages provide many tools for writing modular software, code quality still depends on how carefully those tools are used. This article outlines why modular code matters for long-term maintainability and explores practical methods that facilitate code modularity.
The Causes of Confusing Code
Low-level and early procedural programming languages typically did not offer much beyond basic control flow, conditional jumps, and labels—making it difficult to write modular code. As a result, programs often grew into long, tangled sequences of instructions. Their individual parts were hard to separate, which made them difficult to understand, maintain, and debug.
Today, problems often start when the focus is on ensuring the code works rather than ensuring long-term maintainability. Although beginners tend to be more prone to this, experienced developers aren’t immune. Even code that starts out modular can easily degrade as quick fixes and on-the-fly additions accumulate over time.
How to Avoid Messy Programming
Every well-structured program begins with a clear specification: a document that defines the program’s behavior, valid input values, and expected output. From this foundation, software engineers derive individual modules, applying the same process to each one. The outcome is a set of components with well-defined purposes, supported input ranges, and expected outputs.
This technical document is then implemented as short, understandable, maintainable, and testable source code. Depending on the language, this may involve functions, classes, and inheritance—all of which help organize code into logical, reusable units.
Although modularity plays a vital role in code quality, experienced developers also employ other best-practice techniques that improve a program’s quality without immediately affecting its behavior. Examples include using meaningful variable and function names, using consistent formatting, avoiding code duplication, and writing clear, purposeful comments.
The Benefits of Modular Code
The main benefit of modularity is that it makes code easier to read and understand, helping both the original author and other programmers. When developers can understand code quickly, it becomes much easier to add new features or maintain existing ones.
Modular code reduces duplication, making programs shorter, easier to read, and simpler to maintain. Additionally, developers only need to fix bugs in a single, isolated section rather than in multiple copies of the same code. Smaller, well-defined modules also make testing and debugging easier, since errors are confined to a limited scope and can be traced more quickly. Clear specifications with valid inputs and their expected outputs further facilitate automated testing.
Modular design additionally simplifies adapting platform-dependent programs to new hardware by breaking them into small, focused components with clearly defined tasks. These software modules can be reused across projects or shared with other devs as libraries and packages.
How to Make Existing Code Modular
Creating a specification and deriving clean code works well for new programs, but existing code often requires a different approach. Refactoring consists of methods that improve readability and maintainability without changing program behavior. It focuses on removing code smells, which are signs of poorly structured or hard-to-maintain code.
Refactoring Technique: Split Method
Consider the following concrete example, as it could be found in an ordinary Arduino sketch:
void setup() {
Serial.begin(9600);
pinMode(A0, INPUT);
Serial.print("Temperature");
Serial.print(" sensor registered on ");
Serial.println(A0);
pinMode(A1, INPUT);
Serial.print("Humidity");
Serial.print(" sensor registered on ");
Serial.println(A1);
pinMode(A2, INPUT);
Serial.print("Motor RPM");
Serial.print(" sensor registered on ");
Serial.println(A2);
}
void loop() {
unsigned int sensorA = analogRead(A0);
unsigned int sensorB = analogRead(A1);
unsigned int sensorC = analogRead(A2);
Serial.print("Temperature: ");
Serial.println(sensorA);
if (sensorA > 100) Serial.println("Warning: high temperature!");
Serial.print("Humidity: ");
Serial.println(sensorB);
if (sensorB > 85) Serial.println("Warning: high humidity!");
Serial.print("Motor RPM: ");
Serial.println(sensorC);
if (sensorB > 500) {
Serial.println("Warning: high RPM!");
}
delay(1000);
}
This example program originally only read the temperature, so initializing its analog pin in setup() and reading the value in loop() was sufficient. Over time, additional sensors were added. The developer who added the RPM sensor used different formatting and also forgot to update the value checked in the if condition. Copy-paste errors like this are easy to overlook and difficult to catch. Such mistakes can be largely avoided by writing modular code and reusing functions instead of duplicating similar logic across the program.
Extract Method is a refactoring technique that addresses these issues by identifying repeated code sections and moving them into reusable functions. In this example, each sensor reading involves two steps, both of which can be turned into helper functions:
void initializeSensor(String name, uint8_t pin); void printSensorData(String name, uint8_t pin);
After identifying these steps, developers can move the repeated code into dedicated helper functions. For example, initializing a sensor always involves a pinMode call followed by three output statements:
pinMode(A0, INPUT);
Serial.print("Temperature");
Serial.print(" sensor registered on ");
Serial.println(A0);
The only differences between the duplicated blocks are the sensor pin and name. By turning these into method parameters, the same function can be reused for different sensors simply by passing in the appropriate values each time it’s called.
One of the original copies serves as the basis for the new function. After adding the parameters, the helper function becomes:
void initializeSensor(String name, uint8_t pin) {
pinMode(pin, INPUT);
Serial.print(name);
Serial.print(" sensor registered on ");
Serial.println(pin);
}
The function contains the same code as before, but the static pin number and sensor name are replaced with parameters that can be passed in when the function is called. After extracting this common behavior, all instances of the original code should be replaced with calls to the new function:
void setup() {
Serial.begin(9600);
initializeSensor("Temperature", A0);
initializeSensor("Humidity", A1);
initializeSensor("Motor RPM", A2);
}
Even this simple example highlights the benefits of extracting common code into a helper function. The setup() method becomes easier to read, and the refactored code gives a meaningful name to the steps needed to initialize a sensor. It also links each pin name to the physical quantity the sensor measures.
Similarly, the three repeated blocks in the loop() function can be extracted into another helper. In this case, the warning threshold also needs to be passed as a parameter, along with the sensor name and pin number.
void printSensorData(String name, uint8_t pin, int threshold) {
unsigned int readValue = analogRead(pin);
Serial.print(name);
Serial.print(": ");
Serial.println(readValue);
if (readValue > threshold) {
Serial.print("Warning: High ");
Serial.print(name);
Serial.println("!");
}
}
The loop method can then be simplified to:
void loop() {
printSensorData("Temperature", A0, 100);
printSensorData("Humidity", A1, 85);
printSensorData("Motor RPM", A2, 500);
delay(1000);
}
The extraction not only greatly enhances the loop method’s readability, but it also fixes the copy-paste mistake from before.
Refactoring Technique: Combine Related Values
Even after extracting common functionality into helper functions, some related values are still repeated throughout the code. In this example, all sensor-related information could be grouped into a custom class or structure, allowing developers to pass a single object that contains all the relevant values.
The following code combines information that belongs to a sensor into an easy-to-use and clean structure:
struct Sensor {
uint8_t pin;
String name;
int threshold;
};
The program can then define sensor instances like this:
Sensor temperatureSensor = { A0, "Temperature", 100 };
Sensor humiditySensor = { A1, "Humidity", 85 };
Sensor motorRpmSensor = { A2, "Motor RPM", 500 };
Doing so not only gives each set of sensor details a meaningful name, but also allows simplifying the helper methods from before:
void initializeSensor(const Sensor& sensor) {
pinMode(sensor.pin, INPUT);
Serial.print(sensor.name);
Serial.print(" sensor registered on ");
Serial.println(sensor.pin);
}
void printSensorData(const Sensor& sensor) {
unsigned int readValue = analogRead(sensor.pin);
Serial.print(sensor.name);
Serial.print(": ");
Serial.println(readValue);
if (readValue > sensor.threshold) {
Serial.print("Warning: High ");
Serial.print(sensor.name);
Serial.println("!");
}
}
Finally, the setup and loop functions only have to pass in a single value:
void setup() {
Serial.begin(9600);
initializeSensor(temperatureSensor);
initializeSensor(humiditySensor);
initializeSensor(motorRpmSensor);
}
void loop() {
printSensorData(temperatureSensor);
printSensorData(humiditySensor);
printSensorData(motorRpmSensor);
delay(1000);
}
Summary
This mind map summarizes the key concepts of modular code.
Spaghetti code describes programs that are tangled, hard to read, and difficult to maintain. It’s often caused by copy-paste fixes and focusing on making the code work rather than long-term maintainability. Modular code solves these problems by organizing programs into reusable, well-defined components. It improves readability, reduces duplication, simplifies testing and debugging, and makes extending or adapting code easier.
For new programs, modularity begins with a clear specification that defines behavior, inputs, and expected outputs, which is then implemented through functions, classes, or other logical units. Best practices like meaningful names, consistent formatting, and clear comments further enhance code quality.
Developers need to refactor existing code to make it more modular. Doing so improves its readability and maintainability without changing behavior. The Extract Method is a common technique in which repeated code is moved into a reusable function, with parameters allowing different inputs. Grouping related values into a struct or class can further simplify code and reduce duplication.

