Software Design · 3/3
Modularity in practice: naming the cohesion, then splitting two real systems
A cohesion quiz with real examples, then two decomposition exercises: a condominium payments workflow and a padel club booking system with an AI agent.
On this page
The last post listed the seven kinds of cohesion. A list is easy to memorise and hard to apply, so the practical class did the opposite: take real modules and name the cohesion, then take two small systems and split them into modules, arguing for each choice.
Name the cohesion
For each module, which kind of cohesion holds it together, and why?
| Module | Cohesion | Why |
|---|---|---|
PriceCalculator: only the pricing rules for a sandwich (up to three ingredients a base price, each extra one a smaller increment) | Functional | Every method serves one well-defined task: calculating a price. |
| A commerce pipeline: base price, then discounts, then promotions, each stage updating the order | Sequential | Each stage takes the order the previous one produced and passes it on transformed. Output of one is input of the next. |
CustomerRecord: everything the company knows about a customer (orders, complaints, deliveries, interactions) | Communicational | The parts belong together because they’re about the same data. Under GDPR, a European user can ask for all of it, so a module whose job is to gather it makes sense. |
CheckoutWizard: the UI steps of checkout (delivery, payment, confirmation) | Procedural | The steps must happen in order, but each one isn’t a transformation of the last one’s output. |
A Startup or bootstrap class: everything that runs when an app or OS starts | Temporal | The parts are together because they run at the same moment, not because they’re related. |
| A class with every method in the system whose name starts with A | Logical | Tempting to call it coincidental, but there is a rule, even if it’s a useless one. |
A Misc file where things landed because there was nowhere else | Coincidental | No rule at all. |
The pipeline example came from the professor’s experience with Microsoft’s commerce platform, which had a pipeline infrastructure exactly for this: price, discounts and promotions as a chain of components over the same order. It’s also the clearest way I’ve found to separate sequential from procedural: if data flows through the steps, it’s sequential; if only the order matters, it’s procedural. The two are close, and the checkout wizard is the one to think twice about: it does carry data from step to step, but each step collects something new rather than transforming the last one’s output, so the order is what holds it together. Procedural.
The “letter A” class was the good trick question, and the one I got wrong: I said coincidental. It’s logical: the grouping has a rule, which is exactly what separates the two. The rule being meaningless doesn’t make it no rule.
Exercise 1: condominium payments
The professor is his building’s condominium administrator this year, and every month he does the same thing by hand:
- Export last month’s bank statement as a CSV (there’s no direct integration with the bank) and keep only the credits.
- For each credit, work out which owner paid it.
- Register the payment in the payments spreadsheet, a running log of every payment with its date and amount.
- Update the map: the formatted spreadsheet with a green box per owner per month.
- Issue the receipts.
The owners are a list of their own: fraction, name, and monthly quota.
The hard part: who paid?
Step 2 hides most of the complexity. A statement line has a date, an amount, an origin account and a description, and none of them is reliable on its own:
- sometimes the name on the origin account matches an owner;
- if not, the description may mention the fraction or the owner’s name;
- if not, the amount may give it away: with a 50 € quota, a 100 € transfer is probably two months from someone who pays in pairs;
- and if all else fails, the origin account number may already appear in the payments history, from an earlier month where someone matched it by hand.
That’s a set of rules with fallbacks, and a good sign that it deserves a module of its own.
A functional split
The first decomposition the class proposed was one module per responsibility, plus one per file being handled:
| Module | Responsibility |
|---|---|
StatementReader | Reads the bank CSV. One reader per file format, behind a common interface, if more banks or formats appear. |
PaymentExtractor | Walks the statement lines, drops debits and anything that isn’t an owner payment (rent received, supplier refunds), and works out how many months a payment covers. |
OwnerIdentifier | Given a payment, the owners list and the history, applies the rules above and returns the owner. |
PaymentsLedger | Reads and appends to the payments spreadsheet. |
PaymentsMap | Updates the formatted map. |
ReceiptIssuer | Produces the receipts. |
PaymentManager | The orchestrator: runs the monthly process by calling the others in order. |
A naming detail from the discussion that I liked: the class that reads the statement is really about payments, not movements. Most movements are thrown away; naming the module after what it produces keeps its job clear.
The file modules work like repositories over a database, where the “database” happens to be a CSV and two spreadsheets. That’s what makes the rest testable: OwnerIdentifier doesn’t need to know what an Excel file is.
OwnerIdentifier is also a good candidate for non-deterministic logic. Instead of an ever-growing chain of rules, an agent could get the payment, the owners list and the history, and answer with the most likely owner and how confident it is. The module boundary is what makes that swap cheap: the orchestrator doesn’t care whether the answer came from rules or a model.
Other lenses, all valid
The same system can be split by other kinds of cohesion:
- Sequential: the process literally runs top to bottom, each step feeding the next. A pipeline of five stages is a perfectly reasonable design.
- Communicational: one manager per data store (the CSV, the ledger, the map, the receipts), with an orchestrator talking to all of them.
- Temporal: the whole thing runs once a month, so it could live as one scheduled job.
None of these is wrong. The functional split is probably the easiest to maintain and test, but that’s a judgement for this system, not a rule. The exercise is to look through each lens, see what it makes easy and what it makes hard, and choose on purpose.
Exercise 2: a padel club
The second system was ours to decompose. A club rents padel courts to its members:
- only members can book;
- courts have different prices by time slot, and the popular ones cost more;
- members check availability and prices, book, pay and get a notification;
- if a court is taken, they can join a waiting list for it;
- and one use case from this decade: a chat where a member types “I want to play this week, find me a court for 10 to 12 € in the next three days”, and the system books it.
Nothing here is a fixed sequence, and no single piece of data dominates, so functional cohesion was the natural fit: Member, Court, Reservation, Payment, Notification, WaitingList and a BookingAgent.
Two modules were worth arguing about:
- Is the schedule a module, or an attribute of a court? Pricing by slot and availability suggest a module; on the other hand, splitting it out may be granularity for its own sake. It depends on how much logic ends up living there, and it’s fine to start inside
Courtand extract it later. - Is the booking agent its own module, or part of reservations? Technically it’s different: it talks to a language model and uses tools to read availability and create bookings. Functionally, it is a way of making a reservation. This is exactly the modularity versus granularity tension from the last post, and there isn’t a universal answer.
None of this says where anything runs. A real version would have an app or site, one or more databases, the agent talking to an LLM, and a payment provider, and deciding which modules become separate services is a later and different question: services, event-driven architecture and CQRS come further along in the course. Modularisation is the high-level view of how to split a system into pieces, not the whole design.
What I’m taking from this
- Examples beat definitions. I remember “a pipeline is sequential” far better than the definition of sequential cohesion.
- Data flowing between steps is what separates sequential from procedural. Order alone is procedural.
- A rule, however silly, makes it logical. Coincidental means no rule at all.
- The hard step deserves its own module. In the condominium, identifying the owner is where the complexity is, and a clear boundary around it is what lets you swap rules for an agent later.
- Several decompositions can be right. Look through each lens, and choose knowing what each one costs.
- The job is a level above the code. Code can be generated more and more; deciding how a system is split, and why, is what’s expected of us.