Decomposing into Modules & Components
Decomposing into Modules & Components
Once the architectural skeleton is in place, the analyst and design team face a critical question: exactly where do you draw the lines? Decomposition — the art of slicing a system into smaller, manageable pieces — determines how easy the system will be to build, test, and maintain for years to come. Two forces govern every decomposition decision: cohesion (how well the pieces inside a module belong together) and coupling (how tightly any two modules are tied to each other).
Cohesion: How Tightly a Module Holds Together
Cohesion measures the degree to which the responsibilities inside a single module are related to one another. The higher the cohesion, the easier the module is to understand, test, and reuse. There is a classic hierarchy of cohesion levels, from weakest to strongest:
- Coincidental — elements are grouped arbitrarily (e.g., a "Utils" class that contains PDF generation, email sending, and currency conversion with no logical link). Avoid entirely.
- Logical — elements perform similar operations but are selected by a control flag (e.g., one function that handles
create,update, anddeletebased on a parameter). Better than coincidental, but still fragile. - Temporal — elements execute at the same time (e.g., all system startup tasks lumped together). Acceptable only for initialisation routines.
- Procedural — elements follow a sequence of steps (e.g.,
validateInput → formatData → saveRecordin one module). Common but mixes concerns. - Communicational — elements operate on the same data set (e.g., everything that touches the
Orderentity). Good: data-centric modules are naturally cohesive. - Sequential — the output of one element feeds directly into the next (pipeline style). Excellent cohesion.
- Functional — every element contributes to a single, well-defined purpose (e.g., a module whose only job is Calculate Shipping Cost). This is the goal.
Coupling: How Tightly Two Modules Are Tied
Coupling measures the interdependency between modules. Tight coupling means a change in one module forces changes in another — the classic "ripple effect" that makes systems brittle. Again there is a spectrum, from worst to best:
- Content coupling — one module directly reads or modifies the internals of another (e.g., writing directly to another module's private variables). Catastrophic; never acceptable.
- Common coupling — multiple modules share a single global variable. Fragile; avoid in production code.
- Control coupling — one module passes a flag that controls the logic of another. Signals that the modules should be split.
- Stamp coupling — modules share a composite data structure but use only part of it (passing a full
Customerobject when only the email address is needed). - Data coupling — modules communicate only through simple, well-defined parameters. This is the target: each module knows nothing about how the other works internally.
Drawing Component Boundaries in Practice
Consider a clinic appointment booking system. The analyst has identified four major functional areas from the requirements: patient self-service, clinical scheduling, billing, and notifications. A poor decomposition groups everything that touches a patient record into one giant PatientModule. A better decomposition follows functional cohesion:
PatientRegistration— create and manage patient profiles.AppointmentScheduler— check availability, book, reschedule, cancel.BillingEngine— calculate fees, issue invoices, process payments.NotificationService— send SMS/email confirmations and reminders.
Each component exposes a narrow, explicit interface to the others. AppointmentScheduler does not send emails directly — it calls NotificationService.sendConfirmation(appointmentId) and lets that component decide the channel, template, and delivery. This is data coupling at its best.
Coupling and Cohesion in a Real Design Review
Imagine an online store. The lead developer's first draft has a single OrderManager module that validates the cart, reserves stock, charges the card, sends a confirmation email, and logs analytics events. Every responsibility is bundled together. The coupling problem surfaces immediately: if the payment gateway changes its API, the developer must open OrderManager, risking a bug in the unrelated email logic. The analyst spots this using the simple one-sentence test and recommends splitting the module. The final design becomes four components — CartValidator, InventoryReserver, PaymentProcessor, and OrderNotifier — each communicating only through the orderId.
Interface Specification Between Components
Drawing a boundary is meaningless without defining what crosses it. Every component boundary must be accompanied by an explicit interface contract describing what the component accepts and what it returns. At this stage of analysis the contract does not need to be code — a structured description is sufficient:
This specification is technology-neutral — it works whether the implementation is a Java service, a Python function, or a REST endpoint. The analyst writes it; the developer honours it. This separation of what from how is the essence of component-based thinking.
From the Class Diagram to Component Boundaries
You already know how to draw a class diagram. Component decomposition uses that diagram as input. Group classes that collaborate intensively (high interaction count on the class diagram) into the same component. Classes that interact across future component boundaries should do so only through well-defined associations, ideally with minimal multiplicity and clear directionality. A logistics firm\'s domain model, for example, might show that Route, Stop, and Vehicle are tightly interconnected — they belong in a RouteManagement component. Invoice and PaymentRecord belong in a separate Billing component. The route planner needs only to know the final cost, not how the billing component calculates it.
Cohesion and coupling are not abstract ideals — they are design decisions with direct cost consequences. Systems with low cohesion and high coupling routinely cost two to five times more to maintain than well-decomposed alternatives. As the analyst, your job is to make those decisions explicit and traceable before a single line of code is written.