Splitting a 4,800-Line Checkout Class Under a Deadline
Meridian Retail's checkout squad maintains a single class, CheckoutManager, of about 4,800 lines. It validates the cart, calls the pricing database, applies discounts through 27 nested if-else branches, writes the order row, renders the invoice PDF, and sends the confirmation email. Three squads committed to the same file 41 times in six months, and 12 of the last 18 production defects were traced to it. Merge conflicts are routine. A partner team needs exactly one thing: given a cart, return the final price and the discount breakdown. Engineering leadership has eight weeks and two squads available and is debating two options. Option A splits the class into controller, service, and repository layers. Option B splits it by business capability into Pricing, Order Capture, and Notification components. Neither option is free, and the team still owes customers a monthly discount-policy change during the refactor.
1. Which option does the evidence support, and which cohesion and coupling problems must the refactor actually fix? Ground the answer in the specific numbers given.
Read the worked answer
Primary split is Option B, with layering applied only inside Pricing.
- The 27 discount branches, the email template, and the PDF renderer change for unrelated reasons on unrelated schedules. That is coincidental cohesion: one unit, many unrelated responsibilities. Option A reduces layer coupling but keeps pricing policy, order persistence, and notification inside the same service layer, so the file still changes for a discount tweak and a template tweak. Change amplification survives untouched.
- The 41 commits by three squads are connective coupling in practice: one shared mutable object and one shared file force cross-team coordination on every change. Option B creates ownership boundaries and shrinks the cross-team contract to a price-calculation interface.
- The defect concentration (12 of 18 in one class) is the signature of a unit too large to test in isolation; splitting along capability gives Pricing a rule-level test surface.
- Sequence by risk: extract Notification and PDF first. They are leaf dependencies with few callers and the most frequent, lowest-risk changes.
2. For the 27 discount branches, should the team adopt a Strategy pattern or move the rules into a database rule table? State the criterion that should decide it.
Read the worked answer
Strategy pattern now; a rule table only when a specific trigger appears.
- Strategy: one class per rule implementing
appliesTo(cart)andapply(cart)with an explicit priority, composed by a small factory. The 27 branches become 27 testable units, and stacking/priority becomes an ordering decision instead of nesting depth. Cost: adding a rule still requires a deploy, which the monthly policy change already implies. - Rule table: business users edit thresholds and tiers without a release, and there is an audit trail. But it demands a validator, precedence semantics, effective dating, versioning, and a UI - and interactions between stacking rules become data defects that are harder to test than code.
- Decision criterion: choose the table when the changes are parameter values (percentages, thresholds, tiers) made by non-developers, and Strategy when the changes are new logic. A useful hybrid for Meridian: Strategy for the roughly six structural rules, a lookup table for tier thresholds.
- Avoid a general rule engine here; at eight weeks and two squads it adds an opaque runtime dependency that is hard to debug at 02:00.
3. The partner only needs a price. What should the published contract contain and what must stay hidden? Justify each exclusion.
Read the worked answer
Expose a narrow, versioned, read-only quote operation.
- Input: a cart summary (line items, quantities, customer tier, currency, coupon codes). Output: a quote object with final price, currency, per-line breakdown, applied-rule identifiers, and a
quote_idwith an expiry timestamp. - Hide the
Orderaggregate, database entities, and any parameter that lets the partner pass a discount rule or a computed price. Pricing internals change monthly; a partner-facing contract must be stable, and accepting a client-supplied discount is a trust boundary violation. - The applied-rule identifiers are included deliberately: they let the partner explain a price to its own customer without exposing the rule logic, and they make disputes resolvable.
- The quote id plus expiry exists because price can go stale between quote and acceptance; on accept, re-price and reject if the quote expired. Without this, the contract implies a price guarantee the system does not provide.
- Define error semantics separately: invalid cart is a non-retryable 4xx with field-level errors; pricing unavailable is a retryable 5xx. Partners build retry logic from whatever you publish, so publish it explicitly.