David Parnas gave the definition of a module in Communications of the ACM in 1972, and it still holds up. He described a module as a responsibility assignment rather than a subprogram. The word says how a design divides up responsibilities. It says nothing about how the system gets deployed.
Modular software architecture applies that idea to a whole system. The system splits into self-contained modules, each with a defined interface and hidden internals, so a team can change, test or replace one without touching the rest. Modular monoliths, microservices and plugin-based systems all follow it, and teams judge the result by cohesion inside each module and coupling between modules.
What is modular software architecture?

Each module hides its internals behind a defined interface, and that is what lets teams build, test and replace modules independently of the rest.
Parnas’s 1972 paper is where all of this starts. His argument was that how well a system is modularized depends on the criteria used to divide it into modules.
He expected splitting a system this way to pay off in development time, because separate groups could work on separate modules with little need to coordinate. A module could also change drastically without disturbing the others.
And engineers could study the system one module at a time instead of all at once.
What modular architecture is not
A system is modular when its boundaries exist in the code, whether or not anything runs separately.
Shopify’s old setup shows the opposite case. Inside its monolithic application, the code that calculated shipping rates sat next to the checkout code, and very little stopped either from calling the other (Shopify Engineering, 2019).
Microservices are not the definition either. They are one way to deploy modules that already have clear boundaries.
Put another way, modularity decides how the code is divided, and where it runs is a separate choice made later.
What defines a module?

A good module is cohesive inside and loosely coupled to everything outside, with a public interface that hides the rest. These pull against each other a little, and that tension is where most of the design effort goes.
Cohesion, coupling and encapsulation

Cohesion describes how closely the elements inside one module belong together, so it is an intra-module property. Coupling goes the other way and covers how much one module depends on or interacts with the others. Encapsulation keeps the internals private, which leaves only the public interface reachable.
Adding modules lowers the effort inside each one and raises the integration effort between them. Every system therefore has a module count that minimizes cost, and splitting past it does not help.
Interfaces and dependency direction
The interface is the contract. Everything outside it is private.
Alistair Cockburn’s ports and adapters pattern, better known as hexagonal architecture, puts the interfaces (ports) inside the application and keeps technology-specific adapters outside it. The core never learns what sits on the other side.
Robert C. Martin’s dependency inversion principle, the D in SOLID, states the same rule for classes. High-level code and low-level details both depend on abstractions, never directly on each other.
Most of the time modules talk through a plain in-process call to the public interface, which is simple, synchronous and easy to trace. The other route is a published event, where the sender has no idea who is listening.
Dependencies between modules should form a directed acyclic graph. A cycle means two modules cannot change or ship apart.
What forms does modular software architecture take?

Most real systems end up as a modular monolith, microservices, a plugin-based system or a component-based one. The module boundaries are the same idea in each. What differs is how far a module is isolated at runtime.
| Form | Deployment unit | Isolation | Typical fit |
|---|---|---|---|
| Modular monolith | One application | Code level, one process | Few teams, shifting boundaries |
| Microservices | One per service | Process and network | Independent scaling and ownership |
| Plugin-based | Core plus add-ons | Runtime loading, explicit exports | Extensible tools and platforms |
| Component-based | Reusable packages | Published interfaces | Parts reused across systems |
Component-based systems assemble software from reusable parts that publish their interfaces. Their boundaries serve reuse across systems rather than structure inside one.
Modular monolith
Shopify’s core monolith held more than 2.8 million lines of Ruby and 500,000 commits, according to Shopify Engineering (2020).
Instead of splitting it into services, the company kept everything in a single shared codebase and defined boundaries between components. The goal was more modularity without more deployment units (Shopify Engineering, 2019).
The catch is that boundaries are only as strong as whatever enforces them.
Microservices
Microservices put a network between modules.
In a microservices setup, each service deploys, scales and fails on its own. The price is what Martin Fowler calls the microservice premium, meaning the cost of managing a suite of services, which slows a team down on simpler applications (Fowler, 2015).
Plugin-based systems
A plugin system keeps a small core and loads extensions at runtime. The Eclipse IDE is built this way. The platform is itself a composition of components, called bundles or plug-ins, running on the Equinox implementation of OSGi (Eclipse Foundation, 2006).
OSGi forces explicit contracts between those bundles. A bundle shares nothing by default, anything other bundles may use has to be exported, and whatever it needs from them is imported.
How do you choose between a modular monolith and microservices?

Default to a modular monolith. Split a module into a service only when one concrete need forces it.
Fowler’s monolith-first argument rests on one observation. Almost every successful microservice story started with a monolith that grew too big and was broken up (Fowler, 2015). He adds that microservices only work once stable boundaries already exist between the services.
A team without mature DevOps practices pays the microservice premium in full, because CI/CD, container orchestration and observability all become prerequisites (Jovanovic, 2026).
Stay with the modular monolith while one team, or a few, share a release cadence. The same goes if module boundaries still move between releases, or if CI/CD, orchestration and observability are thin.
A module earns a service of its own once its boundary has held through several releases. Needing to scale on a different profile from the rest justifies it too, and so does a separate team owning the module end to end.
Sources written years apart keep landing on the same order of operations, with boundaries first and deployment second. Fowler (2015) wants stable boundaries before services. Shopify Engineering (2019) drew boundaries without adding deployment units, and Jovanovic (2026) splits along proven module boundaries.
With a modular monolith you keep the boundaries and give up independent deployment between them. Microservices get you that independence, but network calls and operational weight come with it (Encore, 2026).
The advice against microservices as a default is not only practitioner opinion. A 2023 arXiv study by Su, Li and Taibi reports that switching from microservices back to monoliths has become more common, and that the shift has stirred heated debate.
How do you find the right module boundaries?

Draw a boundary where change and vocabulary cluster. That means code the same people edit for the same business reason, and terms that keep one meaning inside it. Start from business capabilities and bounded contexts, then test the draft against the dependency graph and the team structure.
Choosing a boundary
Eric Evans’ domain-driven design defines the bounded context as a boundary, typically a subsystem or the work of one team, inside which a single model applies (Evans, Domain-Driven Design Reference, 2015). One model, one vocabulary.
Business capabilities are the other obvious starting point. These are functions the business already names, such as billing, catalog or shipping. Change frequency adds a practical check, since code that changes together should stay together.
When these disagree, trust the change history over the org chart. The commit log shows what actually moves together.
Conway’s law and team ownership
Melvin Conway’s 1968 Datamation paper states that an organization that designs a system produces a design copying its own communication structure.
Applied to modules, a boundary owned by two teams carries two sets of decisions and one fragile interface. Conway’s own prescription was to organize design work around the need for communication, which for modules means one owning team per boundary.
If the target boundaries cut across current teams, change the teams first. Otherwise the old structure shows up in the code anyway.
How to split an existing monolith into modules
- Map the graph. List which packages call which, and mark every cycle.
- Cluster the code into candidate modules by capability, and check each candidate against a bounded context.
- Match each candidate to one team, or flag it for a team change.
- Publish the few types other modules may use and mark everything else private.
- Move one module at a time, and run the full test suite after each move.
- Write a short architecture decision record with the boundary, the reason and the date.
Each move is a series of small refactorings, so every step stays reversible. A big-bang rewrite offers no such exit.
How to enforce module boundaries in code

Enforcement can sit in the compiler and build system, in architecture tests and lint rules that run in CI, or in module loaders at runtime. Use the lowest level the stack supports, because a violation that fails the compile never reaches review.
Language and build level
The Java Platform Module System, delivered with Java 9 in September 2017, turns encapsulation into a compiler rule. A package is reachable from another module only if its module explicitly exports it (Oracle, 2017).
At the project level, build tools add a second layer.
Maven’s reactor sorts modules so each one builds before anything that requires it (Apache Maven multi-module guide), which is why a cyclic reference fails the build. Gradle’s Java Library plugin splits dependencies into api and implementation, and consumers never see implementation dependencies on their compile classpath (Gradle documentation).
The limit is documented too. Implementation dependencies stay visible to consumers at runtime.
Test and lint level
Architecture rules written as tests fail the build the same way a broken unit test does.
- ArchUnit checks package, class, layer and slice dependencies plus cycles by analyzing Java bytecode inside any plain Java test framework. Version 1.0.0 shipped in October 2022 (InfoQ, 2022).
- Spring Modulith verifies that application modules form no cycles and reach each other only through API packages. A module can also declare an allow-list of dependencies, and everything else is rejected (Spring Modulith reference documentation).
- Packwerk enforces dependency boundaries between Ruby packages by static analysis. Privacy checks were removed from core in version 3.0 and now live in the separate packwerk-extensions gem. Packwerk avoids false positives at any cost, accepts some false negatives, and sees only static constant references (project README and v3.0.0 release notes).
- On the JavaScript side, Nx has an enforce-module-boundaries lint rule that errors on deep imports into another project’s internals, and tag constraints limit which project types may depend on which (Nx documentation, as of October 2026).
Existing violations are the practical problem on an older codebase. Packwerk records them in todo files and tolerates them, so the tool blocks only new ones (Packwerk retrospective, Rails at Scale blog).
Run these checks in continuous integration so a violation fails the pull request instead of surfacing in review.
Runtime level
Module Federation, a webpack 5 feature, lets independent builds share code at runtime. A host loads modules that a remote exposes, and those remote modules are not part of the host’s own build (webpack documentation).
That moves the check from the compiler to the network. Natalia Venditto (micro-frontend.dev, 2023) lists the failures that follow. Version skew happens when modules require incompatible dependency versions, and fate sharing means a bug in one module breaks the app that loads it.
OSGi fills the same runtime role on the JVM.
Benefits and costs of modular software architecture

The benefit is cheaper change, since engineers read, test and replace one module at a time. The cost is boundary design up front and enforcement upkeep at every seam.
Most of the payoff shows up in long-term maintainability. A change that respects the interface stays inside its module.
Changes stay local, so reviews and rollbacks stay small. A module also tests on its own, and Spring Modulith ships integration tests scoped to a single module for exactly that. Ownership gets simpler too. One module maps to one team, so triage and upgrades split along the same lines.
The downsides are easy to underestimate.
- Boundaries cost design time before any feature ships.
- Packwerk flags violations but gives no guidance on fixing them, a gap the Packwerk retrospective names directly.
- Every call across a seam goes through an interface, which adds code.
- Data stays the hardest seam, because modules that read each other’s tables are coupled whatever the imports say.
The benefits are real only where the boundaries hold. A module graph nobody enforces is a folder structure.
When modular architecture fails
Boundaries drawn in the wrong place, services that must ship together, and modularization applied before the domain is understood are the usual ways this goes wrong. There is also a case that is not a failure of the idea, only a poor fit.
Wrong boundaries and cyclic dependencies
Wrong boundaries show up as symptoms before anyone names them. One feature change ends up touching three modules, and two modules cannot be tested without each other. A cycle in the dependency graph is the other giveaway.
The fix is to move code across the boundary, not to add an exception. Each entry on an allow-list is technical debt, and an allow-list that only grows means the boundary is wrong.
Distributed monolith
Services that cannot be changed or deployed independently add up to a distributed monolith.
Google researchers argued in 2023 that microservices conflate logical boundaries with physical ones. Their proposal is to write a modular application as one binary and let a runtime decide placement (Ghemawat et al., HotOS 2023).
Murat Demirbas raised a fair question in a 2023 review, namely what happens to blast radius, bursty traffic and metastable failures once components are co-located.
Treat the proposal as one position in a live debate, not a settled fix.
Premature modularization

Conway wrote in 1968 that the first design is almost never the best possible, so the prevailing system concept may need to change.
In a young domain every boundary is a guess. Each guess comes with interfaces, enforcement rules and tests that must move when the guess is wrong.
Keep modules coarse until the commit history shows which code moves together.
Cases where modular design does not pay
A prototype one person maintains gains little from enforced boundaries.
Prime Video’s video quality analysis team cut infrastructure costs by more than 90% after replacing distributed components orchestrated by AWS Step Functions with a single-process application (Prime Video engineering blog, 2023).
- The orchestration made several state transitions per second of stream, hit account limits, and was billed per transition.
- Video frames passed between components through S3, which meant a heavy volume of reads and writes (InfoQ, 2023).
That was one monitoring service, not the whole platform. The converter and the detectors still exist as separate components, and they now run inside one process.
The lesson is narrow. Modular design does not require a network between modules, and a pipeline that moves large payloads between them pays for every hop.
Modular Software Architecture FAQ
What is the difference between a module and a component?
A module is a unit of decomposition inside one system, defined by what it hides. Components are built for reuse and replacement, which usually means they are packaged and versioned on their own.
One piece of code can be both, but the two words answer different questions.
How do you measure modularity?

Count dependencies in both directions. Afferent coupling is how many modules depend on a given module, and efferent coupling is how many it depends on.
Robert C. Martin’s instability metric divides efferent coupling by the sum of both. A cycle in the dependency graph is the simplest red flag.
How many modules should a system have?
No fixed number exists.
Add a module while it has its own reason to change and one owning team. Stop when a new split adds more integration work than it removes.
Does modular architecture slow an application down?
Not by itself. A call across a module interface inside one process costs no network round trip.
Overhead appears when modules sit behind a network, or when large payloads cross a boundary on every request.
Is domain-driven design required for modular architecture?
No. Domain-driven design is one proven way to find boundaries through bounded contexts, but business capabilities and change history can draw them too.
What a module needs is one owner and a narrow public interface.
Can modular architecture work in a frontend application?
Yes. Frontend code splits into feature libraries, each exposing a public API through a single entry file, such as the index file Nx expects.
Teams that deploy separately can add runtime sharing on top of that structure.
Keeping Module Boundaries Intact After the First Release
Modular software architecture holds its shape when automated checks run on every change and each module has one owning team. Boundaries kept by convention alone have nothing that rejects the next shortcut.
Block dependency cycles in the build first. Freeze the existing violations in a baseline file next, and give each module one owner.
Cycles come first because Maven, ArchUnit, Spring Modulith and Nx all detect them, and a cycle makes every later rule harder to apply.
No single layer closes every gap. Packwerk sees only static references and Gradle leaves implementation dependencies visible at runtime, so a build rule paired with a test rule covers both.
The cost is friction, since every pull request now waits on a boundary check.
- What Communications Teams Should Ask Before Commissioning a Mobile App - November 12, 2026
- How to Install Node JS in VS Code and Run Your First Script - November 11, 2026
- Protecting Users and Brand Standards Across Image-Driven Platforms - November 10, 2026



