Module-First Development: Designing for Testability and Change
Software | . 6 min read (1493 words).
Unit testing and modularity once promised confident change and fearless refactoring. Too often the reality was brittle tests, paralysed teams and slow progress.
Module-First Development (MFD) seeks to restore that promise. It places modules with clear contracts at the centre and directs testing effort where it delivers the most value – at the boundaries of those contracts. MFD is a lightweight framework built on familiar foundations: modularity, encapsulation, unit testing and refactoring. It helps design and test trustworthy modules with strict API contracts, while leaving tools and processes flexible.
Topics:
Executive summary
MFD restores the promise of modularity and unit testing by applying the same discipline to internal modules as to third-party libraries. Each has a documented API contract and strict encapsulation. Core business logic is isolated, deterministic and unit tested. The result is systems that remain dependable and adaptable through continuous change.
Purpose
The goal of MFD is to make continuous change safe. By treating modules as products with contracts that can be trusted, teams can refactor without fear and add features without violating hidden assumptions. MFD does not prescribe how to organise your codebase or which frameworks to use. It defines how modules should be designed, tested and combined so that quality is built in at depth.
What is a module?
A module is a unit of abstraction at any level – a component, a library, an application or a microservice. Its internals are encapsulated. Its boundary is a documented API contract that defines inputs, outputs, preconditions, guarantees, error handling and concurrency behaviour. Consumers depend on the contract, not the implementation.
This is standard practice for third-party libraries, but MFD applies the same discipline to internal modules as well.
Modules are hierarchical and independent of language-level constructs. Their boundaries are primarily logical, not physical, and should be designed rather than dictated by the technology stack. A larger module may contain smaller modules at multiple levels, each with its own API contract.
As an example, a microservice is a module with a network API. Internally, it may contain multiple modules of different types. Third-party libraries are modules too, but MFD focuses on internal (first-party) modules.
Three types of modules
MFD distinguishes three types of modules, each with different expectations. Most systems contain multiple modules of each type.
Core modules – contain business logic and other non-trivial code. They expose deterministic, unit-testable APIs without direct side effects or I/O, and can be tested in full isolation. Side effects are accessed only through abstract interfaces.1 They are unit tested through their public API. Complexity belongs here, along with thorough testing.
Integration modules – adapt external dependencies, I/O and other non-deterministic behaviour into stable contracts. They are thin wrappers that can be mocked or faked and shield core modules against volatility. Integration modules can also provide multiple backends behind a unified API. They are tested through automated, repeatable integration tests.
Glue modules – assemble the system. They handle orchestration, configuration and dependency injection. Glue is deliberately kept simple enough to be verified by inspection and occasional end-to-end tests.
This separation avoids wasted effort. Tests are concentrated where they matter most – unit tests of core modules – and brittle tests for low-risk code are avoided.
As an example, a library may expose a thin top-level glue module with a public API and only minimal direct testing. Internally it relies on unit-tested core modules for complex logic, with integration modules where needed. Applications often have a glue layer at the top level without a public API. Some libraries may instead expose a core module directly as their public API and still achieve good unit testing.
Note: Third-party libraries do not need to be wrapped unless adaptation or isolation is required. Integration modules are for when that is the case.
How to identify module types
The following decision tree can help identify the type of a module:
If the code is trivial enough to be verified by inspection, it is glue.
If it is deterministic, side-effect free and isolatable with a clear seam, it is core.
Otherwise, it is integration.
Note that the goal is to move most non-trivial code into core modules over time.
Pillars
MFD rests on four pillars:
- Modularity – cohesive and loosely coupled modules with documented APIs.
- Encapsulation – strict data hiding so internals can change freely.
- Separation of concerns – distinct concerns in separate modules.
- Testability – API contracts that are explicit and testable (according to above).
Together with the requirements for each module type above, they create systems that can evolve without turning brittle. How these pillars are implemented can vary based on situation and the people involved.
Principles
The principles of MFD guide how modules are designed and tested:
- What changes together belongs together. Design cohesive modules.
- Loosely coupled modules. Design stable APIs that keep changes local.
- Every module is treated as a lightweight product, even internal ones.
- Unit tests verify the API contract – not the implementation details.
- Modules collaborate: core decides, integration does and glue assembles.
- Assert preconditions and make errors explicit. Assume mistakes will happen.
- Treat errors, security, concurrency and performance as part of the API.
- Design deterministic APIs with simple, predictable concurrency models.
- Leaky abstractions indicate a flawed contract and should be refactored.
These principles are not hard rules but valuable guidelines that reinforce the pillars.
Practices
These practices make MFD practical day to day:
- Write new unit tests where applicable for every fixed defect.
- Document API contracts explicitly in the codebase.2
- Write unit tests that each verify one behaviour of the module’s API.
- Refactor glue logic into core modules if it becomes complex.
- Review module boundaries and test coverage3 in code reviews.
- Run CI/CD pipelines on every commit and keep unit tests fast.
- Implement monitoring and observability in production to detect issues early.
- Upgrade dependencies regularly – comprehensive tests make this safe.
- Maintain strong support for debugging and developing individual modules.
- Refactor continuously to keep modules clean and simple.
These practices are recommendations that often apply, but may not be suitable in every situation.
Benefits
MFD addresses several persistent problems in software development:
Business logic becomes testable Core modules are isolated from I/O and side effects, so unit tests can exercise behaviour directly and reliably. Correctness is judged against the documented API contract, which guides testing and avoids arbitrary tests without clear purpose.
Changes stop breaking hidden assumptions Stable contracts at module boundaries prevent surprises in both upstream and downstream code. Breaking changes are detected at the API boundary.
Systems become antifragile Every defect fixed with a new test makes the module more resilient – failure actively strengthens it. Over time the suite of regression tests grows into a safety net, steadily increasing confidence.
Cognitive load is reduced Each module can be understood, tested and reasoned about independently. Teams focus on one contract at a time instead of juggling the whole system.
Loose coupling localises change Clear separation means most changes affect only their own module or have easy-to-understand impact across modules. Refactoring and feature work cease destabilising the wider system.
Refactoring is safe and continuous Since changes are localised and contracts are tested, refactoring can be done continuously without fear of breaking things. This makes it easier to manage technical debt through small improvements along the way, avoiding disruptive cleanup projects.
These benefits reinforce one another and accumulate over time. Teams regain confidence in unit testing, progress accelerates and complexity remains contained.
Why it matters
MFD helps teams build systems that remain maintainable. Core modules become dependable building blocks. Refactoring is safe because contracts hold. Change is fast because modules recombine cleanly. Testing is lean and meaningful, focused on behaviour that consumers rely on rather than implementation details that may change during refactoring. Integration absorbs external change, while glue remains trivial – a thin cushion that connects modules without hiding complexity.
For organisations this translates into faster delivery and lower long-term cost, and for teams it means less brittle testing and more confidence in change.
In short, the confidence that unit testing and good modularity once promised is made real. This is not new theory but a return to first principles that many senior software engineers and architects already apply from experience. By applying this, teams build software that evolves while remaining robust and trusted.
Anything that decouples core modules sufficiently to allow isolated testing qualifies, including dependency injection, logging APIs and well-designed centralised configuration. ↩︎
Documentation can consist of structured code comments, external docs, good naming, static typing or a combination. It varies what is idiomatic in different languages and teams. The key is that the API contract is clear and discoverable without knowing implementation details. ↩︎
Note the difference between test coverage (how well requirements are covered by tests) and code coverage (how much code is executed during tests). Code coverage is a useful tool, but not a goal in itself. ↩︎