Modular Design & Decomposition
Decomposition breaks a system into modules that each do one thing well — so a change in one place doesn’t ripple unpredictably through the rest.
At a glance
Section titled “At a glance”- What it is: Splitting an application into cohesive units (modules, packages, services) each owning one clear responsibility.
- Why it matters: Prevents “spaghetti code,” where every change risks breaking something unrelated.
- For developers: when reviewing a PR, ask does this module do one thing, and would splitting it make the next change easier?
- When to use it: from day one of a new system, and whenever an existing module keeps growing unrelated responsibilities.
- When not to use it: don’t split prematurely into many tiny modules before you understand the domain — that creates its own coordination overhead.
- Prerequisites: Introduction to Software & Application Architecture.
Mental model
Section titled “Mental model”flowchart LR
A["Single 'do everything' module"] --> B[Identify responsibilities]
B --> C[Group related logic together]
C --> D[Cart module]
C --> E[Pricing module]
C --> F[Payment module]
Decomposition is about finding the seams that already exist in a tangled module — grouping logic that changes for the same reason, and separating logic that doesn’t.
Step-by-step walkthrough
Section titled “Step-by-step walkthrough”- List the distinct responsibilities currently mixed together in a module.
- Group logic that changes for the same reason into the same module (the Single Responsibility Principle).
- Give each module a clear name and a small, explicit public interface.
- Move logic shared by multiple modules into its own module, instead of duplicating it or reaching into another module’s internals.
- Re-check module sizes periodically — a module that’s grown multiple unrelated responsibilities again is a signal to decompose further.
Worked example
Section titled “Worked example”A retail company’s Checkout module mixes cart contents, pricing rules, and payment execution in one place.
- Input: a single
Checkoutmodule handling cart state, discount/tax rules, and payment execution together — a pricing bug fix requires re-testing payment code too. - Process: the team identifies three distinct reasons to change (cart contents, pricing rules, payment provider integration) and splits the module along those lines, giving each a small explicit interface.
- Output: separate Cart, Pricing, and Payment modules, each independently testable and deployable, communicating through explicit interfaces instead of shared internal state:
// Each module exposes only a narrow interface — no shared mutable statepublic interface ICartModule{ Task<CartSnapshot> GetSnapshotAsync(string cartId);}
public interface IPricingModule{ Task<PriceQuote> QuoteAsync(CartSnapshot cart, string customerTier);}
public interface IPaymentModule{ Task<PaymentResult> ChargeAsync(PriceQuote quote, PaymentMethod method);}Common use cases
Section titled “Common use cases”| Use case | When to use | Notes |
|---|---|---|
| New system from scratch | At initial design | Start with a small number of clearly named modules; split further later as needed |
| Untangling a “god module” | When one file/module does too much | Identify reasons to change, and split along those lines |
| Shared logic duplicated across modules | When the same logic is copy-pasted in multiple places | Extract a shared module, but keep its interface minimal |
Advanced details
Section titled “Advanced details”- Single Responsibility Principle in practice: “reasons to change” is a better test than “lines of code” for whether to split a module.
- Avoid premature decomposition: splitting before the domain is understood creates coordination overhead that can be worse than the tangle it replaced.
- Modules vs. services: decomposition can stop at a module boundary within one deployable, or go further into separately deployable services — the latter should be a deliberate choice (see Service & Component Boundaries), not a default.
Quick reference
Section titled “Quick reference”| Term | Meaning |
|---|---|
| Module | A cohesive, independently understandable unit of code with one clear responsibility |
| Single Responsibility Principle | A module should have only one reason to change |
| God module/class | A module that has accumulated too many unrelated responsibilities |
| Cohesion | How closely related the responsibilities inside a module are |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| Every feature touches the same file | Responsibilities were never separated | Identify distinct reasons to change and split along those lines |
| Small change requires re-testing unrelated features | Module has multiple responsibilities | Decompose the module, add tests per new module |
| Team split into many tiny modules with no clear value | Decomposition happened before the domain was understood | Consolidate related modules; split only where a real reason to change already exists |
Check your understanding
Section titled “Check your understanding”- What’s the difference between splitting by “reason to change” versus splitting by file size?
- Pick a file you know is a “god module” — name the 2-3 distinct reasons it changes.
Related pages
Section titled “Related pages”Part of Introduction to Software & Application Architecture. See also Service & Component Boundaries for the next step, and the Glossary for term definitions.