
Learn the domain before you design the abstraction
Pattern-driven architecture only works when you've internalised the domain first. Three career moments where the abstraction got better only after the domain understanding did.
There is a particular failure mode I have made enough times to recognise its shape from a distance. A new domain lands on the desk. The team is bright, the deadlines are real, and the engineers — myself included, more than once — reach for the patterns they already trust. Domain-Driven Design, CQRS, event sourcing, some flavour of clean layering. The whiteboard fills up. Aggregates appear. Bounded contexts get drawn with confident black marker. By the end of the second week there is a model, the model is internally consistent, and everyone has agreed on the words.
Then the model meets the domain, and the domain wins.
The most important thing I have learned in twenty-five years of building software is also the most boring to write down: the abstraction comes last, not first. Patterns are tools for expressing a domain you already understand. They are not tools for discovering it. Reach for the pattern before the understanding is in place and you build a structure that is elegant on its own terms and wrong on the terms that actually matter. The people who live in the domain look at your model, are too polite to say so, and quietly work around it.
I want to talk about three moments in my career where this rule got beaten into me, in roughly the order it happened. Each one is from a different industry. Each one started with me thinking I knew what to build and ended with me realising I had not yet earned the right to design it.
Just Eat, 2010: the EPOS printers
I joined Just Eat as one of the first twenty technical hires, back when the platform was being rebuilt out of its student-prototype origins. The headline work was the kind of re-architecture engineers like to put on a CV — pulling an ASP.NET WebForms estate over to MVC 3, layering it properly, putting in an automated deployment pipeline with Ruby and Jenkins, internationalising for two new European markets. That work was real and I am still proud of it. But it is not the part I remember.
The part I remember is the EPOS printers.
A restaurant's electronic point-of-sale terminal is, in 2010, a stubborn little box behind the counter that prints receipts, takes card payments, and decides what the kitchen is cooking next. For Just Eat to actually work as a marketplace — for a customer's online order to land in the right kitchen at the right moment without someone hand-keying it into the till — the platform needed to put orders directly onto those terminals. There are dozens of different printer models. They speak proprietary serial protocols. The vendor documentation, where it exists, was written for someone integrating a single installation, not a marketplace.
I sat with restaurant operators. I watched how they actually used the till during a Friday rush, what they ignored, what they argued about when something jammed. I learned that "an order" is not one thing — it is a header, a payment method, a set of line items with modifiers, a delivery slot, a kitchen routing decision, a printer assignment, and, importantly, a moment of social agreement between the front-of-house and the chef. None of that was visible from the marketplace side of the wire.
If you had asked me on day one to design "the order integration," I would have given you something tidy: a canonical order model, a translator per device, a queue. By the time I had spent enough hours behind enough counters to know what I was looking at, the design had changed. The right abstraction wasn't a canonical order — it was a transmission contract between the marketplace and a deeply local workflow, with the local workflow having veto rights at several points.
This is where the rule crystallised for me, and I have been chasing it ever since. The reverse-engineering of the printers was the easy part — protocols can be sniffed, packets decoded, behaviour reproduced. The hard part, and the part that took weeks, was learning the domain the printers were operating inside: the rhythm of a Friday-night kitchen, the boundary between the website's order and the kitchen's order, the difference between a system that delivers data and a system that respects an existing workflow.
Philips, 2019: the clinicians and the feedback loop
A decade and several companies later, I was at Philips, leading the architecture for what we called the Clinical AI App Store — a container-based platform that ran third-party machine-learning models inside more than three hundred hospitals, on air-gapped VMware estates, using Kubernetes operator patterns to do the kind of lifecycle work that, in a less regulated world, you would do with a kubectl and a coffee. The constraints were technical (ISO 13485, IEC 62304, HA clusters, HL7/FHIR integration with PACS/RIS and imaging equipment) and the geography was global (Germany, India, the UK). The platform piece looked, on paper, like a problem I had solved variations of before. Kubernetes is Kubernetes.
The piece that taught me something new was the diagnosis feedback loop.
A model running on a CT image in a hospital is not a benchmark. It is a recommendation that a clinician will accept, override, or quietly disagree with — and we wanted to capture all three so the model could be validated against real-world results. The mechanism, on the wire, was HL7 lab messaging: when a patient was later given a diagnosis, that message came back into our platform and was correlated against what the model had said.
I drew the first version of the feedback architecture on my own, from the engineer's side. Inference produces a structured output. The diagnosis is a structured output. Match them on patient identifier and study, accumulate, report. Done.
Then I sat with the clinicians, and learned, slowly, that almost every word in that sentence was hiding something.
A "diagnosis" is not a moment — it is a process that unfolds over hours or days, may be revised, may come from multiple specialists, may be encoded against one of several coding systems depending on the hospital, and may legitimately disagree with itself between draft and final. A "patient identifier" in HL7 traffic is several identifiers, depending on message, source system, and local convention. "Matching" is a clinical decision dressed up as a technical one — a clinician will tell you when two pieces of information refer to the same event, and an algorithm deciding on its own will be confidently wrong in ways nobody catches until an audit.
The version we shipped looked different from the one I had sketched. The data shapes were closer to what the clinicians already used. The matching rules were explicit, configurable per hospital, and visible to the people who would be asked, eventually, to defend them. The platform itself was the same Kubernetes and HL7 and FHIR I would have built either way — but the seams in that platform, the places where one concept ended and another began, were drawn by people who had spent careers in radiology and pathology, not by someone who had spent two weeks reading the HL7 specification.
I keep thinking about this when I read about AI systems being deployed into expert workflows. The model is rarely the hard part. The hard part is the boundary the model is sitting on, and only the people who have lived on that boundary can show you where it actually runs.
BrightInsight, 2020: the underwriters and the DSL
The work I am proudest of in my career is the one I am least able to make look exciting on a slide. It is a JSON file format.
At BrightInsight I worked on indemnity-insurance products, where the legal constraint is unusual and severe: given the same answers to an indemnity questionnaire, the system must produce the same decision over a specified period. That is a determinism requirement that sits at right angles to the way modern teams ship code. Daily releases mean the code under the questionnaire keeps changing. Legal compliance means the answer must not change. Reconciling those two requirements naïvely — by freezing releases, or by branching by product, or by adding manual review gates — destroys the engineering practice. Doing it well requires the rules to live somewhere other than the application code.
The team's instinct, and mine, was to design the rule engine first. Sketch a domain model. Decide what an "indemnity rule" is. Build the evaluator. Then hand it to the underwriters.
We did not do that. Or rather, we tried, and the rules the underwriters actually had — the ones in policy documents, in training material, in the heads of senior staff — refused to fit. The categories we had drawn were too clean. The constructs we had named were not the constructs underwriters thought in. The objects in our model were not the objects they were trying to express.
So we slowed down and went the other way. We sat with the underwriters. We watched them build a product on paper. We watched them describe how a question's answer changed which subsequent question got asked. We watched them argue, gently, with each other about whether a particular combination of answers really did imply a particular indemnity outcome. We took the JSON structure that had begun life as a configuration file and we let it grow, slowly, toward what they were actually trying to say. By the end it was a domain-specific language, in the literal sense — a language for the underwriters' domain, not ours.
Two things mattered about the result. First, it was deterministic: the same answers produced the same outcome, because the answer was a function of data the underwriters owned, not of code we were continuously deploying. Time-windowed regression retesting against every previously answered questionnaire gave us the legal guarantee, and CI gave us the engineering cadence. Compliance and continuous delivery stopped being in tension, because the rules and the application were no longer the same artefact.
Second, and this is the part I care about more, the underwriters owned the product. Not "the underwriters wrote tickets for the engineers to translate." The underwriters wrote products. The DSL was theirs in the way that SQL belongs to data analysts and spreadsheets belong to finance teams — not perfectly, not without help, but as a primary working medium. We had not abstracted the domain away from them. We had given them an abstraction of their domain that they could pick up.
That only happened because we had spent the time, before designing the language, to understand what the language was for.
What changes when you do this
What changes, when you treat the domain as the source of truth and the abstraction as the consequence, is mostly invisible from outside the team. The architecture diagrams look similar. The patterns are still recognisable — there is still domain-driven design, there are still aggregates and bounded contexts and reconciliation loops and rules-as-data. Eric Evans's original DDD work is still the canonical reference, and the patterns Martin Fowler has been cataloguing for two decades still travel. The vocabulary does not change.
What changes is that the joints in the system end up in different places. The seam between the marketplace and the restaurant is not where the engineer's instinct would have drawn it. The seam between inference and clinical truth is not where the platform team would have drawn it. The seam between rules and code is not where a clean-architecture diagram would have drawn it. In each case, the joint is where the domain already has one — and the abstraction's job is to make that pre-existing joint legible, not to invent a new one.
The other thing that changes is who is empowered. When the abstraction comes from the domain, the people who live in the domain remain its authors. Restaurant operators kept owning the front of house. Clinicians kept owning the diagnosis. Underwriters kept owning the product. The platform served as the medium through which their expertise reached production, rather than as a translation layer that they had to negotiate with through tickets. This is not a soft point. It is the difference between a system that scales linearly with the engineering team and a system that scales with the experts the engineering team supports.
How I try to work now
I do not have a methodology for this. I have a small set of habits I notice myself reaching for whenever I land in an unfamiliar domain.
Sit with the people who do the work, more than once, and not on a call. Watch them do the thing. Take the notes you would not normally take — about what they grumble at, what they ignore, what they double-check. The interesting design constraints are never in the requirements document; they are in the muscle memory.
Build a working glossary before you build a model. The words domain experts use are load-bearing, and the moment you replace one with a "better" engineering term you have started building the wrong abstraction. If they say "indemnity," do not say "claim." If they say "study," do not say "image set." The wrong vocabulary metastasises into the wrong domain model six weeks later.
Stay close to the messy real-world data before you stabilise on a shape. HL7 traffic from one hospital is not HL7 traffic from another. EPOS protocols from one printer family are not from another. Underwriter spreadsheets are full of corner cases the policy document does not mention. The abstraction has to survive the corner cases; the only way to know what they are is to look.
Design the abstraction late and visibly. Once you do reach for the pattern — DDD, CQRS, rules-as-data, whatever — name what you are doing, write down why it fits, and check the fit against the people who would feel the cost if you got it wrong. The patterns are still the right tools. They are just the last tools, not the first.
The mistake I no longer make is designing the model first and hoping the domain fits. The version of me that joined Just Eat in 2010 would still have built something workable, because the patterns are good and the engineers were good. But the version of me that learned to sit behind the till on a Friday night, and to sit with clinicians on a feedback loop, and to sit with underwriters on a JSON file, builds things the people in the domain recognise as their own.
That is the difference. The pattern is the same pattern. The understanding underneath it is what makes it land.
— Madu