The 5 Essential Elements of Modular Software Design
Complexity kills developer productivity. Thankfully, modular design gives us complexity-fighting superpowers.
Why do we care about modular design?
Why do we care about modular design? Let me tell you a story about my friend Mike Kelly. In 2016 he took a side job to build a simple course platform. Since it was a side job, and Mike was bored, he offer to do this job at a deeply discounted rate if he could retain rights to the source code. It was a brilliant move. That client had reach, and other people started asking about his work. “This is a really nice platform. It’s really easy to use. It’s fast, and it works well. What is it?”
Mike decided to test the waters. His plan was to package up the product so it would be easier to custom-install on each clients’ hardware. He quickly realized it was going to be a nightmare to maintain the software in each client’s environment. Instead, he moved towards a software-as-a-service solution, hosting each client’s course in one place for a monthly fee. Thus MemberVault.co was born.
MemberVault was a success. More and more people signed up. Mike quit his day job and, together with his wife, started working full time on marketing, sales, customer support, bugs and new features. There was just one problem. In the rush to get things out the door, he didn’t have time to change his original design: each client required their own database. Each new customer, free or paying, added another database to his MySQL server.
Fast forward to 2019 and MemberVault’s MySQL server had thousands of databases. MySQL is not designed to scale this way, and performance and memory problems were starting to creep up. Mike had to solve this problem soon. He was proverbially tied to the train-tracks and the locomotive was barreling down on him.
This is where I came into the story. I helped Mike think through the possibilities and we came up with a simple solution. We decided it was best to stick with MySQL, but refactor the code to use a centralized database. It would need a new schema with an added clientId field for most tables. The question was how to refactor the existing code-base, migrate old customers, minimize downtime, minimize development time, and avoid having to maintain two codebases for the different database schemas during the transition.
The solution ended up being astonishingly simple (the best solutions always are). Mike added the clientId field to the existing databases even though they didn’t need it. He could refactor the code in place, test it and deploy it incrementally before he introduced the new, centralized database. Once everything was working on the existing databases, he could add a small tweak to his existing database routing logic to route all new clients to the central database, which would have the exact same schema. He could worry about actually migrating the old clients later, at his leisure, and still only have to maintain one code-base.
The problem was Mike’s code was a mess. It wasn’t modular. It had grown organically as he struggled to keep up with all the needs of a successful, growing SAAS company. For example, instead of being wrapped up in a single, well-designed module, the code for adding users was duplicated and spread across multiple code-bases: member-user-signup, member-admin, membervault-admin, cron jobs, the API and other external integrations.
Modular design could have reduced the amount of code Mike needed to update by a factor of 5 and dramatically reduced the complexity of the overall refactor. Instead of taking months, the refactor could have been done in a couple weeks. That’s the power of modular design. Modular design allows code to remain agile in the face of ever-changing requirements.
Modular design allows code to remain agile in the face of ever-changing requirements.
Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality - wikipedia
As with all software engineering, the ultimate goal of modular design is to maximize developer productivity. We want to create maximum software value for minimum development cost. Modular design amplifies developer productivity by managing complexity.
Complexity is the primary killer of developer productivity. Software complexity can scale exponentially with size. Thankfully, modular design gives us complexity-fighting superpowers. It allows us to decompose big, seemingly complex systems into small, manageable parts. The strength of a project’s modular design will ultimately determine how large it can scale. Without modular design, it would be impossible to manage the complexity of all but the smallest, most incremental projects.
Modular design gives us complexity-fighting superpowers.
With good modular design, there is no limit to how high and how far we can go with software. Modules give us the super power of abstracting arbitrary complexity behind clean, simple interfaces. Modular design can, when wielded by masters of the craft, transform complexity which scales exponentially with project size to a project where complexity scales sub-linearly. It is even possible for a project to get easier to add functionality as the project grows.
The 5 essential Elements of Modular Design
The goal of modular design is to manage complexity. To do this we must simultaneously minimize the complexity of each module as well as the overall module-dependency network. Practically, the goal is to make each module as easy as possible to design, implement, test, deploy, upgrade and maintain. The 5 elements of modular design are essential to achieving this goal.
Make each module as easy as possible to design, implement, test, deploy, upgrade and maintain.
purpose: A module is an abstraction with purpose. Its purpose should be crystal-clear. It should have a single, exclusive responsibility. A module’s responsibility should be narrow and focused, and no two modules’ purpose should overlap.
(single responsibility principle)
solution: To the client, a module is just a solution to a problem. The solution is a concept of how to solve the problem and a method for solving it. The client doesn’t care how it works, only how to use it. Therefor, the solution is embodied in the interface, the API. A module’s interface should be easy to use, easy to understand and easy to ensure correctness. It should offer all this without needing to understand any of its implementation details.
To achieve this, a module’s API should be well-defined and documented. The API should be complete and minimal. It should have exactly what is needed and nothing more. Last, it should be hard to misuse. The easiest way to use a module should also be the correct way.
(Arnaud’s three principles of excellent API design)
encapsulation: A module’s implementation is private. Modules should expose as little as possible. They should not expose their functional structure, data-structure nor their own dependencies. Any implementation detail of a module should be changeable without affecting a single client.
This is perhaps the most important element of modular design. The other four elements could all be expressed in terms of maximizing the isolation as much as possible of the internal implementation from the outside world. As an abstraction, each module should as watertight as possible. Leaks become accidental, hidden parts of the public API. Without strong encapsulation, you end up with implicit dependencies which can be disastrous to scaling projects.
(Joel’s law of leaky abstractions
foundation: A module’s foundation is its implementation. It should be correct, performant, tested and minimal. In order to ensure the module is easy to use it must work well. A module with the best-designed purpose, interface and encapsulation will still fail without good implementation. The worse the implementation, the leakier the abstraction.
connection: The connections between modules adds their own complexity to the overall system. A well designed modular system minimizes the dependencies between modules. Minimize each module’s external dependencies and periodically review the overall modular-design to look for complexity-decreasing opportunities.
What Shouldn’t Be a Module
As with any technique, modular design can go too far. If you factor a 100,000 line program into 20,000, 5-line modules, you may be creating just as much mess and headache as putting all 100,000 lines in one file.
Even if a chunk of code meets the five elements of modular design, it may still be overkill to factor it into its own module. There are two things to be wary of if you feel like you are making too many modules:
mono-dependency: If a module is only used once in another module, a parent, it may not make sense to make it its own module. It’s ok, even good, to have modules which have only one dependency. These are often called sub-modules. As long as they solve a distinct sub-problem for their parent, as well as adhere to the other essential elements of good, modular design, they can be essential to managing the complexity of that parent.
On the other hand, if the code is used two or more times, especially in different modules, it should almost certainly be a module in order to keep things DRY.
mostly-ceremony: If you have a module that you think may be unnecessary, ask how much code it would save to merge it into its parent. If it is a mono-dependent module, you will almost always save a little code, but that alone isn’t a good reason to de-modularize. However, if you save something closer to 90% of the module’s code size just by folding it into the parent, that’s probably a code-smell you don’t want to ignore.
A module with two or more dependencies should almost certainly be a module in order to keep things DRY.
Subjective Reality of Modularization
The goals of modularization is always to decrease complexity and increase clarity. These are ultimately subjective judgements. Everything I’ve discussed in this article is secondary to your team’s well considered judgement for what works on your particular project.
Learn the rules well so you know how to break them properly - Dalai Lama
Benefits of Modularization
The primary benefit of modularization is mastering the art of managing software complexity. When done well, good modular design leads to some very powerful, practical benefits:
understandability: A well-modularized system is much easier to reason about, think about and communicate to others.
improvability: Strongly encapsulated modules maximizes your ability to fix or improve individual module implementations without needing to update any other, dependent modules.
refactorability: The less inter-dependencies in a project, the easier it is to make large changes across multiple modules.
reusability: Modules with the best-conceived purposes are fully reusable. Whenever you have the same problem again, you can simply reuse the old solution.
testability: Modules with good solutions and minimal inter-dependencies are easier to test. Well-designed solutions, as defined by the module’s API, can be easily unit-tested. Modules with minimal inter-dependencies can be tested without the need for mocks or more difficult integration-testing. Modules allow you to write tests once, ensure correctness, then reuse the module without the need for further testing.
scalability: All of which adds up to the most important benefit of modules: They let our applications scale. It’s impossible to build large applications without good modularization. Without modules, complexity will destroy your productivity.
Without modules, complexity will destroy your productivity.
Higher, Further, Faster
Modules allow us to climb higher, go further and build faster. Modules are the building blocks of the virtual landscape. We’d be nowhere without modules. We can only build the amazing software possible today because of the amazing foundation of existing modules we can draw upon. A programming language is a module. Operating systems are a collection of modules. The most successful languages have vast module libraries (module counts by language). Some modules have endured decades of use and continue to be key to our collective success. That is the power of good modular design.
Modules allow us to climb higher, go further and build faster.
So get out there! Take your modular design skills to the next level and build something awesome!