Skip to content

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.

  • 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.
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.

  1. List the distinct responsibilities currently mixed together in a module.
  2. Group logic that changes for the same reason into the same module (the Single Responsibility Principle).
  3. Give each module a clear name and a small, explicit public interface.
  4. Move logic shared by multiple modules into its own module, instead of duplicating it or reaching into another module’s internals.
  5. Re-check module sizes periodically — a module that’s grown multiple unrelated responsibilities again is a signal to decompose further.

A retail company’s Checkout module mixes cart contents, pricing rules, and payment execution in one place.

  • Input: a single Checkout module 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 state
public 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);
}
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
  • 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.
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
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
  • 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.

Part of Introduction to Software & Application Architecture. See also Service & Component Boundaries for the next step, and the Glossary for term definitions.