Skip to content
Episode 29 · The rule that lived in eleven places

What is software architecture?

Tuesday, 10:40: changing free cancellation from 24 to 48 hours took six days and eleven files, and the admin panel still says 24. Thirty-seven guests were refused a refund they were owed.

Architecture isn't boxes on a whiteboard. It's the handful of choices that would hurt to undo later.

30 min intermediateWhat architecture isCoupling & cohesionLayered architectureHexagonal architecture (ports & adapters)Architecture decision recordsTrade-off thinking
  1. Problem

    A booking app grows from 1 to 14 engineers. Every change starts touching everything, and nobody remembers why things are the way they are.

  2. Why the simple system fails

    Without deliberate structure, business rules leak into controllers, SQL leaks into templates, and big decisions get made in chat threads and forgotten.

  3. Engineering concept

    Architecture = the important, hard-to-change decisions: boundaries, dependencies, data ownership, and how parts communicate. Decide them on purpose and write them down.

  4. Technology

    Layered and hexagonal (ports & adapters) structures, module boundaries, Architecture Decision Records.

  5. Trade-offs

    Structure costs indirection and ceremony. Too little and change becomes expensive; too much and simple features take a week.

01

The problem

2 min
Tuesday, 10:40. Your first week on the Booking team.
  1. Maya · Product manager
    Free cancellation went from 24 to 48 hours. It took six days, and the admin panel still says 24.
  2. Omar · On-call SRE
    37 guests were refused refunds they were owed.
  3. You · Engineer at StayHub
    It's one number. How can that take six days?
  4. Lina · Senior engineer, your mentor
    Guess first. Then we'll search the code together.
02

The big idea

4 min
Why a one-line rule took six days
The big idea

is the choices that are expensive to change later: where code and data live, and who may depend on whom. They set the price of every future change.

Not boxes on a whiteboard. In a hotel, repainting a room takes an afternoon; moving the elevator shaft is a construction project. Architecture is the elevator shafts: the few things everything else leans on.

03

How it works

5 min
How a rule spreads, and how it comes back
  1. 1
    Year 1: one file

    Two engineers, one controller checks "24 hours". Everything fits in two heads.

  2. 2
    Years 2–3: copies

    The admin panel, a refund job and the PDF each need the rule. Each team copies it.

  3. 3
    Year 4: drift

    14 engineers. A change misses one copy, and screens disagree. Guests notice.

  4. 4
    Fix: one home

    A CancellationPolicy in the core, called by every entry point. That's high : the things inside one part belong together.

  5. 5
    Fix: loose edges

    The core asks for refunds through a port (an interface it owns: "I need refunds"); the payment SDK sits in an adapter that plugs into it. That's low : one part needn't know or change with another.

A useful sorting question: one-way door or two-way door? A date library is two-way: swap it in an hour. The main database, a public API 1,800 hotels integrate with, or who owns which table are one-way: slow down and write the decision down.

04

Watch a decision being made

4 min
Worked example

Lina gives the cancellation rule one home

The rule exists in 11 places and just drifted. Make the next change a one-file change, without a rewrite and without stopping feature work.

  1. 1Find every copy, name one owner

    Search for 24, freeCancel, refundable; list each hit and its entry point. Owner: the Booking area. Templates and the PDF only display the result.

    Why: You can't consolidate what you haven't found, and a rule with two owners drifts again. Ownership is the architecture decision; the code follows it.

  2. 2Give it one pure home

    Write the rule once, with no HTTP or SDK inside.

    ts
    // core/booking/cancellation-policy.ts
    export const FREE_CANCEL_HOURS = 48;
    export function canCancelFree(b: Booking, now: Date): boolean {
      return hoursBetween(now, b.checkIn) >= FREE_CANCEL_HOURS;
    }

    Why: Pure code is testable in milliseconds and callable from the API, admin panel and jobs.

  3. 3Point every entry point at it; payments behind a port

    Replace the 11 copies with calls; templates receive freeCancelUntil as data. The core declares PaymentGateway; the SDK lives in an adapter.

    Why: Until the last copy is gone, two answers are still possible. The port keeps vendor details out of the rule, and tests can use a fake.

  4. 4Lock the boundary

    A lint rule: only adapters/payments imports the payment SDK; no cancellation logic outside core/booking. A short decision record says why.

    Why: Unchecked boundaries erode within months. The record tells the next engineer it was on purpose.

The whole thing in one line: Find the copies, pick one owner, one pure home, every screen calling it, a port for the provider, a lint rule to keep it: the next policy change is one file.
05

In real life

3 min
Two companies, two structures

Shopify: one big app, strong modules inside

  • Has publicly described keeping its core as one large Rails codebase
  • Split into components with declared boundaries, checked by tooling

Fits when you want boundaries without running many systems.

Amazon: many services, each owned by a small team

  • Has publicly described services that talk only through APIs
  • Each team owns, deploys and runs its own; needs strong tooling and on-call

Fits many teams that must release independently.

06

What goes wrong

3 min
Treating architecture as diagrams

Architecture lives in what imports what and who owns which data.

Instead

Check the real dependency graph; enforce boundaries with tooling.

Copying a business rule into each screen

The next change misses one copy, and guests get different answers.

Instead

One home per rule; screens only display the result.

Spending a week on two-way doors

Reversible choices don't deserve committee time.

Instead

Decide fast on those; invest analysis in one-way doors.

07

Trade-offs

3 min
StructureFits whenYou pay
Framework defaultsPrototypes, 1–3 people validating an ideaRules spread; change cost grows
Typical business apps, one teamLogic depends on data-access details
Rich rules, several integrations, long-livedMore interfaces and wiring
Friday, 11:00. Planning the loyalty programme.
  1. Karim · CTO
    Loyalty points ship in three weeks. Put the code wherever is fastest. We'll clean up later.
  2. Lina · Senior engineer, your mentor
    "Later" is how cancellation ended up in eleven places. One module with one interface first: two extra days.
  3. Karim · CTO
    Two days is 10% of the deadline, for a feature that might flop.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • Which team owns each business rule (free cancellation belongs to Booking) and which parts only display it. Claude will put the rule wherever the file it's editing happens to be.
  • Whether a choice is a one-way door (the partner API, who writes to bookings) or a two-way door (a date library).
  • Where boundaries are enforced, written in CLAUDE.md: which folders may import the payment SDK, which module defines cancellation logic.
Tell Claude Code
The free-cancellation rule (now 48 hours) is copied in about 11 places: booking API, admin panel, refund job, invoice PDF, 7 templates. Change nothing yet. List every place that computes the rule, with file and whether it computes or only displays. Then propose a plan: one pure function in core/booking owns it, every entry point calls it, templates receive the value as data, refunds go through a PaymentGateway interface. Same behaviour, no new service. Add tests for 47h, 48h, 49h.
Check before you accept
  • Compare Claude's list with your own search. A list of 6 when the incident said 11 means it stopped looking.
  • After the change, exactly one file contains 48 for this rule; only the payments adapter imports the SDK.
  • Tests cover 47, 48 and 49 hours, and you saw them run.
Red flags
  • It fixes the number in the file you pointed at and says "done", leaving the other copies.
  • It proposes a new microservice or a rewrite for a one-function problem.
  • The new core function imports the database client or the payment SDK.
09

Check yourself

2 min
Q1/3Reasoning
Which of these are architecture decisions? Select all that apply.

Select all that apply.

How confident are you?

Next episode: Karim reads a blog post over the weekend and wants to split StayHub into twelve microservices by summer.

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

Moving the rule into the core

Two common shapes for the inside of an app:

  • : controllers → services → repositories → database. Each layer calls the one below. Simple, and the default in most frameworks. Its weakness: business rules depend on data-access details.
  • (ports and adapters): the core defines interfaces for what it needs; databases and providers plug in from outside. Every dependency points inward.
Worked example

Lina gives the cancellation rule one home

The free-cancellation rule exists in 11 places and just drifted. Make the next change a one-file change, without a rewrite and without stopping feature work.

  1. 1Find every copy

    Search the code for the rule's traces: 24, freeCancel, refundable. List each hit with its entry point: API, admin panel, refund job, invoice PDF, 7 templates.

    Why: You can't consolidate what you haven't found. The list also shows who must review the change.

  2. 2Name one owner

    Decide which part owns the rule: the Booking area. Templates and the PDF only display the result; they never compute it.

    Why: A rule with two owners drifts again. Ownership is the architecture decision; the code follows it.

  3. 3Give it one home

    Write the rule once, as plain code with no HTTP or SDK inside.

    ts
    // core/booking/cancellation-policy.ts
    export const FREE_CANCEL_HOURS = 48;
    export function canCancelFree(b: Booking, now: Date): boolean {
      return hoursBetween(now, b.checkIn) >= FREE_CANCEL_HOURS;
    }

    Why: Pure code is testable in milliseconds and can be called from anywhere: API, admin, jobs.

  4. 4Point every entry point at it

    Replace the 11 copies with calls to canCancelFree. Templates receive freeCancelUntil as data instead of computing it.

    Why: This is the step that removes the drift. Until the last copy is gone, two answers are still possible.

  5. 5Put the provider behind a port

    The core declares interface PaymentGateway { refund(bookingId) }. The Stripe-style SDK lives in an adapter that implements it.

    Why: Refunds stop leaking provider details into the rule, and tests can use a fake gateway.

  6. 6Lock the boundary

    Add a lint rule: only adapters/payments may import the payment SDK; nothing outside core/booking may define cancellation logic. Write a short decision record saying why.

    Why: Boundaries that aren't checked erode within months. The record tells the next engineer it was on purpose.

The whole thing in one line: Lina found the 11 copies, gave the rule one owner and one pure home, pointed every screen at it, put payments behind a port, and locked it with a lint rule: the next policy change is one file.
LayeredHexagonal (ports & adapters)
Dependency directionTop → bottom, logic depends on data accessEverything points inward to the core
Swap the database / providerTouches service layerWrite a new adapter
Unit-test business rulesOften needs a DB or heavy mocksPlain fakes, millisecond tests
CeremonyLowMore interfaces and wiring
Good fitCRUD-heavy apps, small teamsRich business rules, several integrations, long-lived products

The senior method, for any one-way door

  1. Problem

    What are we actually solving? Here: a rule change costs six days and still drifts.

  2. Requirements

    One answer for every screen; a policy change in under a day.

  3. Constraints

    14 engineers, feature work can't stop, no rewrite.

  4. Options

    Leave it and add tests; a shared helper file; a core module with ports; a separate service.

  5. Trade-offs

    Tests catch drift but keep the copies. A service adds a network for no gain. A core module removes the copies at modest cost.

  6. Decision

    Core module with ports, written down with a revisit trigger (e.g. if Booking needs its own release schedule).

11

Interactive simulation

12

Visual diagram

Click a component

A booking core in hexagonal (ports & adapters) style. The core in the middle depends on nothing outside it.

13

Analogy

  • In a hotel buildingWalls, plumbing, elevator shaftsIn softwareArchitecture decisions
  • In a hotel buildingRepainting a roomIn softwareA two-way door
  • In a hotel buildingOvens, fridges and sinks in one kitchenIn softwareHigh cohesion
  • In a hotel buildingAll power running through room 101In softwareTight coupling
  • In a hotel buildingStandard power socketsIn softwarePorts and adapters
  • In a hotel buildingThe archive of building decisionsIn softwareDecision records (ADRs)

Where the analogy breaks: a building's walls really are fixed. In software, even "one-way doors" can be undone. It just costs a lot of time and money.

14

Check your understanding

Q1/3Reasoning
In hexagonal architecture, the booking core imports the PostgreSQL client so it can save bookings efficiently.
How confident are you?
15

Practice ladder

Step 1 · Trace

Trace a tourist-tax change

Scenario Warm-up

Lisbon raises its tourist tax from €2 to €4 per night. In StayHub's code today:

  • checkout.controller.ts computes the tax to show the total
  • invoice-pdf.ts computes it again for the invoice
  • admin/booking-edit.ts computes it when staff change dates
  • price-summary.html shows the tax using a value passed in by the controller

Someone updates only checkout.controller.ts. A guest books 3 nights in Lisbon, then support moves her stay by one day.

Trace what the guest sees on the checkout page, the invoice, and after the admin edit. Then say which files would change if the rule had one home.

Step 2 · Complete

Finish Lina's plan for the late-checkout fee

Design Practice

Lina's plan to give the late-checkout fee rule one home has two gaps. Today the rule ("€15 per hour after 11:00, max €60") is in the guest app API, the front-desk admin panel and the invoice job.

  1. Find every copy: search for lateCheckout, 11:00, 60.
  2. Name one owner: ___
  3. Give it one home: a pure lateCheckoutFee(booking, checkoutTime) function in the core.
  4. Point every entry point at it: ___
  5. Lock the boundary: a lint rule, plus a short decision record.

Fill in steps 2 and 4, and say why each matters.

Step 3 · Solve

Untangle the hotel commission

Design Practice

The Hotel Partners team wants to change commission from a flat 15% to "15%, or 12% for hotels with more than 500 bookings a year". Today commission is calculated in:

  • the monthly partner payout job
  • the partner dashboard (shows "what you'll earn")
  • a finance CSV export
  • two SQL views used by reporting

The payout job also calls the bank-transfer provider's SDK directly. Propose a structure that makes this and future commission changes cheap. Say where the rule lives, how each caller uses it, how the bank provider fits in, and how you'd stop the copies from coming back.

Step 4 · Decide

One-way or two-way door?

Decision Stretch

This week three decisions land on your desk. You have one afternoon for careful analysis.

  1. Which date library to use in the new loyalty screens.
  2. The JSON format of the new partner availability API that 1,800 hotels' channel managers will integrate with.
  3. Whether to move the email templates from one templating engine to another.

Pick the one that gets the afternoon, and write a short decision in this form: "We'll spend our analysis on X rather than Y because…, accepting the risk of…". Say how you'd handle the other two.

16

Explain it yourself

Explain to a junior developer what software architecture is, and why the team writes ADRs.

0 words