Systems as Mirrors
Why confusing code often reflects a decision nobody made, and how to write that decision down A customer opens the app and reads ‘You’re approved.’ Two days later, the money still hasn’t arrived. Their identity checks haven’t finished. Nothing has crashed. Each team’s code does exactly what that team meant. To scoring, The most confusing systems I’ve seen don’t reflect complexity. They reflect ambiguity. Conway’s Law says systems mirror the communication structures of the organisations that build them. It’s usually read as a point about org charts: split teams by department, and the system splits the same way. But communication has gaps, and systems mirror those too: our indecision, our shortcuts, the choices we didn’t make. Three teams share one field and never agreed on what it means. That field is the conversation they never had. The code isn’t broken. It’s accurate. The usual response is to fix the code anyway. Product adds a Three flags, eight combinations. What does it mean to be approved and need review? Each flag answers one team’s question. Nobody answers the shared one: when can we tell the customer? So every screen, email and report that reads the flags answers it again, slightly differently. You can’t fix a reflection by polishing the mirror. The fix starts with a question rather than a refactor: what is each team actually deciding? Scoring decides whether to lend, and how much. Compliance decides whether it’s allowed. Product decides what to tell the customer. Three decisions. One boolean was never going to hold them, and neither were three. Domain-Driven Design has a name for this. Each team works in its own bounded context, a part of the system where every word has one meaning. Give each context its own type, and let it say what it means: A limit exists only once an application is accepted. Product no longer reads anyone else’s fields. It takes both answers and decides what the customer sees, in one place: Each line is a rule, and Next time, it can’t be skipped. If scoring adds a new outcome, say a conditional offer, the compiler points at this function until someone decides what the customer should see. The new code is just as accurate as the old one. It still mirrors the teams that built it. This time, there was a decision to reflect. A system can only be as clear as the decisions behind it. Further reading:approved means the risk checks passed. To product, it means the customer can be told. To compliance, it means identity has been confirmed, since in their world nothing is approved before that. One field, three meanings, each reasonable from inside its own team.kycPassed flag and checks both. Scoring adds needsReview for applications an underwriter has to see. A few months later, the shared record looks like this:Application = { approved : Bool, kycPassed : Bool, needsReview : Bool }-- Scoring
Decision = Accepted Limit | Declined | Referred
-- Compliance
Verification = Pending | Verified | RejectedReferred can say ‘needs a person’ and Pending can say ‘not yet’, which no boolean could. The same application is now described once per context, in that context’s own terms. Duplication isn’t the enemy. Imprecision is.-- Product
Status = InReview | Offer Limit | Closed
status : (Decision, Verification) -> Status
status (Declined, _) = Closed
status (_, Rejected) = Closed
status (_, Pending) = InReview
status (Accepted limit, Verified) = Offer limit
status (Referred, Verified) = InReview_ matches anything. A customer sees an offer only when scoring has accepted and compliance has verified. This function is the conversation the teams never had, written down.