From Analysis to System Design

Decomposing into Modules & Components

18 min Lesson 3 of 10

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, and delete based 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 → saveRecord in one module). Common but mixes concerns.
  • Communicational — elements operate on the same data set (e.g., everything that touches the Order entity). 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.
Practical test: Can you describe the module in one short sentence without using the word "and"? If yes, cohesion is likely high. "Handles customer registration" — good. "Handles customer registration and generates invoices and sends emails" — split it.

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 Customer object 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.
Design mantra: Aim for high cohesion, low coupling. These two goals reinforce each other — a module with a single clear purpose naturally has fewer reasons to depend on other modules.

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.

Clinic Booking System — Component Boundary Diagram Clinic Booking System — Component Decomposition PatientRegistration createProfile() getPatientId() AppointmentScheduler checkAvailability() bookAppointment() BillingEngine calculateFee() issueInvoice() NotificationService sendConfirmation() sendReminder() Patient & Schedule Database (shared data store) patientId apptId apptId, fee All inter-component calls pass only simple identifiers (data coupling). No component accesses another's internals.
Figure 1 — Four cohesive components with data-coupled interfaces in the Clinic Booking System.

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.

High Cohesion vs Low Cohesion — Before and After Cohesion Comparison — Online Store Order Flow BEFORE (Low Cohesion) OrderManager validateCart() reserveStock() chargeCard() sendConfirmation() logAnalytics() 5 unrelated responsibilities One change breaks everything AFTER (High Cohesion) CartValidator validate() InventoryReserver reserve() PaymentProcessor charge() OrderNotifier sendConfirmation() Each module does one thing Changes are isolated & safe
Figure 2 — Refactoring a low-cohesion monolithic module into four high-cohesion components.

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:

Component: AppointmentScheduler Operation: bookAppointment(patientId: Integer, slotId: Integer): AppointmentId Pre-conditions: slotId exists and is marked Available; patientId is a registered patient Post-conditions: Slot status set to Booked; Appointment record created; returns new appointmentId Errors: SlotNotAvailableException, PatientNotFoundException

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.

Common mistake — shared database as a hidden coupling point: Two components that are supposedly independent but read and write the same database tables without an explicit interface are actually tightly coupled through the database schema. Any schema change becomes a system-wide event. Wherever possible, each component should own its data and expose it only through its interface. When a shared store is unavoidable (as in the clinic example), document it explicitly and treat schema changes as cross-component contracts.

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.

Deliverable at this stage: A component diagram (UML or informal box-and-arrow) that shows each component as a named box, the interfaces it provides (with a small lollipop notation or a labelled arrow), and the interfaces it requires from others. This diagram becomes a key section of the Design Specification Document you will write in Lesson 7.

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.