How to decompose a project for long term maintainability?
Strategies to decompose system into modules
Howdy friends!👋, welcome to the #2 issue of the newsletter.
Quick Update: The name of the newsletter has changed to Decoupled, which I find more relevant for a newsletter that focuses on software design and architecture. Anyway, let’s dive into today’s post.
Last week, I ordered a few pieces of furniture from IKEA. As I assembled them, I was amazed by the modularity of the furniture. Each part of the furniture is designed as an independent component so they can be built or replaced easily. I suppose the parts can be reused to build several other pieces of furniture, too. This modular component-building technique strikes me with an analogy for decomposing a system into modules in a software project. For example, just as IKEA can reuse a table leg design across different furniture lines, well-decomposed software modules can be reused across different features or projects. In this post, I will discuss the strategies used to decompose a project or a problem into modules using a classic research paper.
But first, the basics..
In software design, modularity is the practice of decomposing a system into independent parts. For instance, in a modern web application, we might have separate modules for:
Payment processing (handling different payment providers like Stripe, and PayPal)
User authentication (managing login via email, Google, or social media)
Product search (using database queries or search engines like Elasticsearch)
Modularisation helps to reduce complexity and promote loose coupling between the subsystems. In the context of this blog, I would like to quote (just like the author of the paper) that module means a responsibility assignment instead of simply a sub-program in the system.
Decomposing the system into modules
The concept of modularity is not new in software engineering. It originated in the 1960s and 1970s. In 1972, David Parnas introduced the key criteria to be used in modularity, in the classic paper titled On the Criteria To Be Used in Decomposing Systems into Modules. Despite being written in the era of punch cards and mainframes, Parnas's insights about hiding design decisions are relevant in today’s agile software development practices with rapidly changing technologies and requirements. Here’s the excerpt of the conclusion from the paper:
We have tried to demonstrate by these examples that it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.
The paper's main idea is that module decomposition should be based on design decisions in the system. The module then should hide those decisions from other modules using information hiding technique (also known as encapsulation). When the design decisions are hidden inside a module, any changes within the module are less likely to propagate in other modules. For example, when you do not expose the data structure used by a module, the change of data structure (such as from Array to Map)
would only affect that module.
In the paper, Parnas uses two approaches to decomposition. The first one uses the system's data flow as a criterion, while the second approach uses the system's design decision for decomposition. He then compares the two modules based on different aspects such as flexibility, independent development, and comprehensibility.
Example: A simple text editor
Let's explore these concepts using a text editor - a system complex enough to demonstrate real design decisions yet familiar enough for everyone to understand. Image a simple text editor like Notepad or Nano. Just to keep things basic, assume the text editor can only perform the following minimal operations:
Accept input from input devices (keyboard and mouse)
Parse the input into text
Formats the text for display on the screen
Render the text on the screen
Allow users to create a new file and save or open existing files
Temporal decomposition
When the modules are organised based on the sequence of operations a text editor performs, the following would be the modules:
Input handler - This module handles input from the keyboard/mouse. It uses an array to store the input.
Text Parser - Receives the array input from the input handler module and parses it into text
Display formatter - Accepts the array input from the parser and formats it for display for screen renderer
Screen renderer - Display the text received from the display formatter on the screen
File manager - Handles loading or saving file
This decomposition follows the data flow in the text editor:
Input → Parser → Formatter → Display → Storage
Did you notice any problem with this decomposition approach? Coupling, information leakage, idk, idc.. ? Anyway, the following are some of my observations:
Complexity in adding features
Adding a syntax highlighting feature requires changes in the text parser and display renderer.
Adding a new file format requires a change in the text parser and file manager modules.
Adding a line break feature affects both the display formatter and screen renderer modules.
Implementing undo and redo functionality impacts almost every module
Maintenance Challenges
Changes ripple through the pipeline
Modules are tightly coupled
Testing becomes more complex
Development Constraints
Teams must coordinate closely
Changes require a full system understanding
Hard to work on modules independently
Design decision-based decomposition
Now, when the modules are decomposed using the design decisions (which module is responsible for which action) as suggested by Parnas, the decomposition results in the following modules:
Text storage - It is responsible for storing text. It contains methods for inserting, deleting, and retrieving text content at specific positions.
Input handler - Responsible for handling input and converting them into operations. For example, it provides the interfaces to handle typing (convert keypress into character), shortcuts (Ctrl + C, Ctrl + V), etc.
Text Parser - This module contains clear interfaces to parse text into structure data.
Display manager - Responsible for visual representation of text on the screen (wrap, highlight, font rendering). It also defines clear interfaces for view-specific functions such as text selection highlighting, scrolling, and viewport management.
File format - It handles different file formats supported by the editor.
History manager - Tracks and manages the changes in the file. It records the operations in the editor and provides an interface for undoing/redoing changes.
In the above approach, each module hides a design decision that might change. The design is improved in the following ways:
It provides flexibility in changing the internal working of a module without rippling the effect on other modules. For example, changing the data structure from array to rope would only impact the Text storage module.
Teams can work independently on different modules because they’re loosely coupled.
It reduces cognitive load in understanding the parts of the system. For example, a developer can understand text parser modules in isolation which was not the case in the first decomposition.
This approach aligns with modern software development needs too.
Teams can work independently on different modules.
New features can be added with minimal impact
Testing is simpler as modules are well-encapsulated
Changes are localized to specific modules
Takeaways
The way we decompose systems into modules has a lasting impact on development efficiency, maintenance costs, and team productivity. While it might be tempting to follow the flow of data when designing systems, hiding design decisions within well-defined modules provides much better long-term benefits.
Just as IKEA's modular furniture design allows for easy assembly, replacement, and reuse of components, good software modularity enables similar benefits in software development. The principles Parnas outlined fifty years ago remain relevant today, helping us build more maintainable and adaptable software systems.
Thank you for reading this article. If you enjoy reading it, feel free to share it with friends!