The 5 Essential Elements of Modular Software Design (PIE-CI)
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 offered 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 Design
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 a project where complexity scales exponentially with 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 individual module and minimize the complexity of the overall system. 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) - interface: 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 be 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) - connection: In order to truly make our software scalable, it’s not enough to just minimize the complexity of an individual module. We must also minimize the complexity of the entire ecosystem in which the module functions. The dependencies between modules define a network which adds its own complexity. A well designed modular system minimizes the dependencies between all modules. To minimize the connections between modules, minimize each module’s dependencies.
- implementation: 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. A module’s implementation should be correct, performant, tested and minimal. The worse the implementation, the leakier the abstraction.
Here’s an acronym I use to help me remember the 5 elements: PIE-CI. I pronounce it “pie-see-eye.”
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 APIs and minimal inter-dependencies are easier to test. Well-designed APIs 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 collections 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!
Higher, further, faster, baby! — Captain Marvel
Further Reading
In part 2 I apply the 5 elements of modular design to a real world problem — how to write scalable client-side applications with complex state:
2021 Update: I could never remember the names I came up with for the five elements. That isn’t a good sign! So, I adjusted their names and now the five are much easier to remember with the acronym: PIE-C-I. Do you agree? — SBD
Originally published at https://www.genui.com.