Add VAT to European orders
Prices shown and charged must include VAT for customers in the EU, at a rate that depends on the country and the product category.
What Makes Software Hard to Change
Make order creation idempotent and auditable
Two identical order-creation requests must produce one order and one charge, and finance must be able to reconstruct every state change afterwards.
Functional and Non-Functional Requirements
Storage becomes a purchasable add-on
Customers can buy additional storage in 50 GB blocks, on any plan, and enterprise customers may have a negotiated limit that ignores both.
Requirements Are a Snapshot
A new reporting pipeline gets write access to adjust balances
A data team ships a pipeline that corrects historical balances from a reconciled ledger, writing directly to the wallets table because that is what their tooling does.
Where Invariants Live
Inventory moves to a separate service owned by the warehouse team
Inventory becomes a service with its own database, owned by a different team on a different release cadence. Order management stays where it is.
Consistency Boundaries
Referral payouts become percentage-based and tiered
Referrers earn 20% of the referee's first invoice, rising to 30% after five successful referrals, and credit expires 90 days after issue.
Problem Decomposition
A requirement that crosses a textbook split
Orders placed by enterprise customers skip payment capture, defer to invoicing, and must be visible in the admin list as "awaiting PO" — one requirement, one requester.
Single Responsibility, Carefully
Add a reason code to cancellation, then delete the trial feature
Two ordinary requests, a month apart: cancellation must record a reason code, and the trial feature is being retired entirely.
Decomposition by Folder
A new payment source, then a new payment concept
First: accept an internal wallet balance alongside cards. Then, a quarter later: support split payment — part wallet, part card, on one order.
What an Abstraction Actually Is
An ordinary change, under a speculative interface
Add a field to the notification payload — a routine product request, unrelated to any variation the interface was built to allow.
What an Abstraction Costs
The third export arrives and it is not like the first two
A transactions export, 40 million rows, must stream to object storage rather than build a string in memory.
The Rule of Three
Make checkout survive contention
Under a flash sale, concurrent checkouts on the same SKU must not oversell, must not deadlock, and must retry safely where the failure is transient.
Leaky Abstractions
Support asks for one behaviour change
Support notifications must thread onto an existing conversation, which means a new header, a different reply-to and no digest batching.
Premature Abstraction
Per-contract negotiated pricing
Enterprise customers get a negotiated price computed by a formula specific to their contract, which may reference volume tiers, committed spend and a custom floor.
Choosing the Model
Add "no more than three units of a restricted item"
Regulated products get a per-customer cap of three units, checked whenever the cart changes and again at checkout.
Encapsulation
Move invoice storage from Postgres to object storage
Invoice documents move out of the `invoices.pdf_bytes` column into an object store, with the old rows read-only during a dual-read window and deleted after verification.
Information Hiding
The vendor ships a breaking SDK release
The provider renames employment status values, changes pagination from offset to cursor, and moves termination dates into a nested object. Routine for them, twice a year, every year.
Stable Boundaries
Export format and deadline change for data-subject requests
Personal data exports must be delivered in a new machine-readable format within 72 hours, with an audit record of who requested and who fulfilled.
Cohesion
Promotions gains a rule: free shipping over a threshold
A promotion can now waive shipping, which means promotions and shipping genuinely need to interact — the connection that was accidental becomes a requirement.
Shared-State Coupling
The schema team renames campaign.discount_pct to campaign.rate_basis_points
A column the pricing path reads is renamed and its units change. No business rule changes; it is purely a storage change made by a different team.
Dependency Direction
Audit asks: reproduce the interest calculation as it ran on 2024-03-31
A regulator requires that any past interest calculation can be re-run and produce byte-identical output, including for accounts whose terms have since changed.
Volatile Dependencies
Add bank transfer as a payment method
Customers can pay by bank transfer, which confirms asynchronously — sometimes days later — rather than at checkout time.
Open/Closed, Critically
Add `bulkAnonymise` for a GDPR requirement
Compliance need a bulk anonymisation operation over users. It is used by exactly one consumer, a nightly job.
Interface Segregation, Critically
Add electric vans, then hybrids
Vans may be petrol, electric or hybrid. Running cost, range and the service schedule all depend on the powertrain; registration, plates and body type do not.
Composition Over Inheritance
Two requirements, priced under both designs
First: finance adds a partner tier. Second: every pricing rule must also return a human-readable explanation for the invoice.
Strategy
Vendor ships a breaking major version
The provider releases v5: `PaymentIntent` is restructured, the error hierarchy is replaced with error codes, and minor-unit handling changes for zero-decimal currencies. Old version is supported for six months.
Adapter
Two requests, six weeks apart
First: "bulk price updates need an audit record". Second: "and they must be previewable, undoable for ten minutes, and resume if the worker dies".
Command
Add a nullable `costCentre` field, end to end
Orders gain an optional cost-centre code, set at creation, shown in the API and in one report. No new behaviour, no new rule.
Pattern Overuse
Reservations expire after one hour, and expiry must refund the payment
If stock cannot be shipped within an hour of payment, the reservation expires, the customer is refunded automatically, and ops gets a daily count of expiries by warehouse.
Domain Modeling
The credit limit must exclude cancelled lines and apply to refunds
Cancelled lines no longer count toward the limit, and a refunded amount frees up limit immediately.
Aggregates
Add one rule: "confirmed orders for new customers require a manual review"
A seventh business rule is added, affecting order confirmation, and it must apply to every path that can confirm an order — API, admin UI, bulk import, retry job, CSV tool, partner webhook.
The Anemic Domain Model
New rule: an order may not ship until the payment capture has settled, not merely authorized
Finance requires that dispatch waits for settlement rather than authorization, because of a rise in chargebacks on authorized-but-unsettled captures.
State Ownership
Retry only what is safe to retry
Payment attempts that failed because the provider was unreachable or slow should be retried up to three times with backoff. Declines must not be retried, and bugs must not be hidden by the retry.
Error Modeling
One more condition in the dunning rule
Annual subscriptions get five dunning attempts instead of three before cancellation, and the fifth email has different copy.
Functional Core, Imperative Shell
The entitlement rules grow from three to twenty-three
Product adds twenty entitlement rules over two quarters: seat limits, regional restrictions, trial extensions, add-on features and a grandfathered legacy plan.
Purity and Testing
Pending authorisations must be held back from spendable funds
The balance a customer may spend now excludes pending card authorisations, while statements, interest and reconciliation must keep using the full ledger balance.
Naming
Add a second payment provider
Accept a second payment provider for customers in one region, with different failure codes and a different capture flow, without changing anything about how orders are priced or confirmed.
Long Functions
Change the retry policy from 3 attempts to 5 with jitter
The retry policy for outbound provider calls changes from three fixed attempts to five with exponential backoff and jitter, and support needs to be able to explain the new behaviour to customers.
Documentation Decay
"The tax rule belongs in Pricing, not in the checkout handler"
VAT is computed inside CheckoutController. The discount rule already lives in a Pricing module. A reviewer says tax belongs there too.
Review as Design Feedback — and Why It Arrives Too Late
Extracting a shared validator that two features happened to share
Usernames must now allow two characters. Product tags must not.
Extract Function
Two requirements, priced before and after the extraction
First: "VAT for Ireland moves to 23%." Second: "every order needs a `channel` field, used in pricing, routing and the audit log."
Extract Module
`Order.total` is restructured to carry currency
`total: number` becomes `total: Money`, with an explicit currency, because the business added a second currency.
Move Responsibility
Add "pause subscription"
A customer can pause billing for up to three months; the account stays active, invoices stop, and the paused period does not count towards the annual commitment.
God Object
Eligibility becomes a rolling twelve months
Loyalty eligibility changes from "spent over 500 this calendar year" to "spent over 500 in a rolling twelve months", with the same threshold and a new time window.
Duplicate Knowledge
The same pricing change, six months apart
Add a promotional discount that applies to the order subtotal before tax. Identical requirement both times.
Interest: Why Debt Compounds
Add a second currency to invoice totals
Invoices must be issued in the customer's local currency, with the exchange rate fixed at the moment of issue and recorded on the invoice.
What "Legacy" Actually Means
Refunds on partially shipped orders, and the two tickets after it
Refund only unshipped lines; then, three weeks later, exclude non-refundable shipping fees; then, a month after that, support partial refunds of already-refunded orders.
The Legacy Change Loop
Support fulfilment from multiple warehouses per order
An order's lines may be fulfilled from different warehouses, with per-line tracking and per-warehouse inventory reservation. The current schema assumes one warehouse per order, in a column on `orders`.
The Risk in a Rewrite
A requirement arrives mid-migration: contact details must support a second email address
Customers may have a secondary email for billing. Read paths must return both; write paths must validate both.
Incremental Migration
Splitting credentials out of `users`, two ways
Move authentication fields to their own store so authentication can be rate-limited and audited independently.
Designing the Migration
A second change to Money arrives — support a currency with three decimal places
Add support for currencies with three minor-unit digits (KWD, BHD), which the fixed division by 100 cannot express.
Expand and Contract
Add a promotional discount to checkout
Apply a percentage discount code at checkout, affecting the displayed total, the charged amount and the confirmation email.
Feature Flags and What They Cost
Prices must be stored and shown in the customer's currency
Every amount in the system acquires a currency. Arithmetic must refuse to mix currencies, rounding must respect each currency's minor units, and historical orders must keep the amounts they were charged.
Change Amplification
Add a referral code to signup
Signup accepts an optional referral code, validates it against active campaigns, stores it on the account, and includes it in the welcome email.
Speculative Generality
Pause Subscription, under layer-first and capability-first
A customer can pause a subscription for up to three months. Billing prorates the current period, renewal is suspended, the state transition is validated, and an event is published for the notification system.
Package by Feature
The feature-flag client ships a breaking major version
The vendor renames its evaluation API, changes the shape of its context object, and drops a deprecated method. The upgrade is not optional because the old version stops receiving security patches.
Stable Dependencies
Two changes, priced under the same design
Change one: replace the payment provider. Change two: add a "purchase order number" that the customer types, the system validates, stores, and shows on the invoice.
Architecture Boundaries
The same standard, applied to two different services
In billing: "late fees are waived for customers on an annual plan in their first 90 days." In the CRUD service: "orders now carry a purchase-order reference."
Clean Architecture, and Where It Is Overused
One requirement, two deployment shapes, four engineers
"When a trial converts to paid, apply the referral credit, start the billing cycle, and notify the referrer." It spans accounts, billing and notifications — and two weeks later the definition of "converts" changes.
Designing a Monolith
The finance team changes how money rounds on invoices
Rounding moves from round-half-up to banker's rounding for tax-inclusive line items, to match a regulator's guidance.
Shared Libraries
Second requirement: "pause for three months, then resume automatically"
A pause now carries an end date, and the subscription must return to active on that date without anyone touching it.
Designing a Feature Before Writing It
"Support needs one reference that finds everything about a pause"
A customer reports a failed pause. Support wants a single value from the error screen that returns the request, the transition, the queue message, the nightly job's decision and the email.
Stable Identifiers
"Renewals are a day early for some customers, some months"
Find and fix a date-boundary bug that only manifests in far-forward timezones during a DST transition week.
Time as a Dependency
Extract a helper, then change a rule
First: extract `RoundingPolicy` out of `PriceCalculator` because three classes were rounding differently. Then, separately: VAT-inclusive prices must round per line rather than per order.
What a Unit Is
Add a second payment provider, and make the flow retry-safe
Introduce a fallback provider for cards the primary declines, and make the whole flow safe to retry after a timeout.
Designing for Failure
Extract the inventory module into a service
Inventory becomes a separately deployed service. The code moves unchanged; only the transport is new.
What Changes at the Network Boundary
Admins may export data for their whole organisation
The self-service CSV export must also be available to organisation admins, returning every member's data — and to support engineers, returning it with the risk score removed.
Designing for Security
The report should also suspend customers over their quota
Product wants the monthly report to suspend any customer more than 20% over their plan quota, and note the suspension on the account.
Least Privilege as a Design Decision
Finance wants the unpaid list as a nightly CSV, for every customer
A scheduled job must export every unpaid invoice across all customers to a file, while the admin page keeps working as it does today.
Cost-Aware Interfaces
The report must cover twelve months instead of thirty days
Product extends the reporting window, so the same code now processes an order of magnitude more rows per request.
Premature Optimization, Reclaimed
Requirement: "data backfills must not appear in the audit log"
A compliance reviewer notices that last month's backfill produced forty thousand audit entries attributed to a service account, and asks for them to stop and for the existing ones to be removed.
Simple Is Not Easy
Q3: "growth wants SMS confirmations for the checkout flow, live before the campaign"
A second notification channel is genuinely required, with a launch date three weeks out.
YAGNI, With Its Bill Attached
Two changes, priced under both failures
Requirement A: deliver scheduled reports to S3 as well as by email. Requirement B: add a `generatedAt` timestamp to every report.
Over-Design and Under-Design
The three billing features the business asked for this quarter
Usage-based pricing for one product line, a new tax jurisdiction, and per-customer invoice scheduling.
Refactor or Rewrite
Framework major version: middleware composition and lazy loading both change
Move a payments service to the next major version before security patches end in five months.
What a Framework Charges
The HTTP client we use ships a major version that changes its error type
The library's 3.0 replaces thrown errors with a result object and renames two options. There is a CVE in 2.x, so staying is not an option.
Dependency Management
Pricing must carry a currency
Every price must be produced with an explicit currency rather than assuming the country's default. The rule itself is a day of work.
API Stability
The payment provider changes its webhook signature scheme
The provider is deprecating its old signature format with a hard cutoff in eight weeks. The code change is about two days of work.
Bus Factor
Subscription pausing, and then "pause should also suspend entitlements"
Build subscription pausing across billing, entitlements, notifications and reporting. Then, six weeks later, the requirement arrives that a paused subscription must also suspend feature entitlements immediately.
Design Review
The refund cap drops from 200 to 100, effective immediately, and finance wants sign-off
The maximum auto-approved refund becomes 100. Anything above it goes to a human. Finance needs to be able to state, on the record, that no auto-refund above 100 is possible.
Designing a System That Has a Model In It