Oh My Learning

Why domain complexity drives design choices

learning-ddd/01 mid drafted · 10 min read

Learning Domain-Driven Design (Vlad Khononov), Part I, Chapter 1 - "What Is Domain-Driven Design?"

Why domain complexity drives design choices#

TL;DR#

Software design decisions should be justified by the complexity of the business problem they serve, not by technology preference or habit. DDD exists because most software failures are not caused by bad code, but by a model that does not match how the business actually works - so the first design question is always "how complex and how differentiating is this part of the business?", not "which framework should we use?".

The idea#

Every non-trivial software system sits downstream of a business problem. The business problem has its own complexity - some parts are genuinely hard, full of exceptions, negotiated rules, and edge cases that took years for domain experts to learn; other parts are simple, well-understood, and essentially the same in every company that does them (sending an email receipt, storing an address, generating a PDF invoice layout). Domain-Driven Design's founding insight, as Khononov frames it, is that software complexity should mirror business complexity, not exceed it and not underserve it. A system fails when engineers either over-engineer the simple parts (building a flexible plugin architecture for something that will never change) or under-engineer the hard parts (treating a genuinely intricate pricing engine as a CRUD form because "it's just a database update").

This reframes what "good design" means. In many engineering cultures, design quality is judged by internal properties: is the code clean, are the layers separated, is it testable. DDD adds a prior question that determines how much of that investment is warranted at all: what does this specific piece of the business need? A generic problem (user authentication, payment card storage, sending transactional email) rarely needs a rich, bespoke domain model - buying a well-tested product or library is usually smarter than modeling it yourself. A problem that is core to why the business wins or loses in its market deserves the deepest modeling investment the team can afford, because that is where a better model translates directly into competitive advantage. Getting this differentiation wrong is, in Khononov's account, the single biggest predictor of a DDD initiative producing bloated, over-modeled trivia alongside a competitor's genuinely differentiating logic left thin and generic.

This lesson is the foundation for the rest of the subject: learning-ddd/02 gives this idea a name (subdomain classification), and everything from bounded contexts (learning-ddd/03) through architecture alignment (learning-ddd/12) is, at bottom, a consequence of taking domain complexity seriously as the primary design input.

How it works#

The core claim: complexity is not uniform across a system#

Consider an e-commerce company. Its system includes: shipping-label generation, tax calculation, product catalog browsing, customer support ticketing, and a dynamic-pricing/promotions engine that adjusts prices per customer segment, inventory level, and competitor pricing in real time. All five are "part of the system." None of them deserve equal design investment.

  • Shipping-label generation follows a well-documented carrier API and printing standard used by thousands of companies identically. There is no competitive edge in inventing your own approach - buy a library or a SaaS integration.
  • Tax calculation is a legally defined, externally-imposed rule set. Getting it correct matters enormously (compliance risk), but getting it architecturally novel does not - again, buy (e.g., a tax-calculation API) rather than build.
  • Product catalog browsing is a solved UX/data problem; a reasonably generic content model suffices.
  • Customer support ticketing is useful but replaceable by an off-the-shelf tool; the company does not win customers because its ticketing logic is clever.
  • The dynamic-pricing engine, by contrast, directly determines margin and conversion rate - competitors cannot simply copy it because it is tuned to this company's specific data, inventory, and customer behavior. This is where investing in a deep, expressive, carefully modeled domain model pays for itself many times over.

Worked example - a SaaS billing platform#

Take a B2B SaaS company selling a project-management tool with usage-based billing. Map the system's parts by "how much unique, hard-won business knowledge lives here":

  1. User authentication and password reset. Extremely well understood industry-wide; a mistake here is embarrassing but the logic itself carries no competitive differentiation. Low complexity that matters (security), low differentiation - buy (Auth0, Clerk, or a vetted library), don't build a bespoke domain model.
  2. Sending email/SMS notifications. Generic infrastructure concern. Low complexity, no differentiation - use a transactional email provider's SDK directly; do not model "Notification" as a rich domain concept.
  3. Usage metering and tiered/overage billing rules. This is where the SaaS company's actual pricing strategy lives - proration rules, grace periods, dunning logic, discount stacking, currency-specific tax rounding, contract-level overrides negotiated by sales. This logic is genuinely intricate and is where the company's commercial strategy is encoded. High complexity, high differentiation - this deserves a carefully designed domain model, its own bounded context (learning-ddd/03), and the team's best modeling effort.
  4. Generating a PDF invoice for download. Complex-looking (layout, formatting, multi-language) but not differentiating - a competitor's invoice PDF being nicer or uglier does not move revenue. Worth solving well, but with an off-the-shelf templating library, not a hand-rolled domain model.

Notice the pattern: complexity and differentiation are two separate axes, and design investment should track the combination, not complexity alone. This two-axis view is formalized in learning-ddd/02 as the subdomain classification (core / supporting / generic).

Worked example - healthcare scheduling#

A hospital's outpatient scheduling system has to handle: patient booking, provider (doctor) calendar management, insurance eligibility checks, and appointment-conflict resolution when a provider double-books or an emergency bumps a routine slot. On the surface all four "involve scheduling." But provider-conflict resolution, when a hospital differentiates itself by minimizing patient wait times and maximizing provider utilization under real-world unpredictability (emergencies, no-shows, multi-resource procedures needing a room and equipment and a specialist simultaneously), is where the hospital's actual operational advantage is won or lost. Insurance eligibility checking, by contrast, is dictated entirely by external insurers' APIs and rules - there is no room for the hospital to innovate there, so a thin integration layer is the right amount of design effort, not a rich domain model.

The anti-pattern this lesson guards against#

Khononov opens the book by naming the trap directly: teams that read about DDD's tactical patterns (entities, aggregates, value objects - see learning-ddd/08) and start applying them everywhere, uniformly, regardless of whether the underlying business logic warrants it. The result is "big ball of mud with extra ceremony" - all the overhead of rich domain modeling (more classes, more indirection, more concepts for new engineers to learn) with none of the payoff, because the effort went into parts of the system that never needed it. The corrective is always to ask the complexity question first, before reaching for any tactical or strategic pattern.

Pros#

  • Prevents both failure modes at once: wasted over-engineering on generic problems, and dangerous under-modeling of the business's actual differentiators.
  • Gives teams a shared, defensible language for prioritizing engineering effort - "is this core to our differentiation?" is a question product and engineering can answer together.
  • Directly informs staffing and hiring decisions: your best, most senior engineers and closest collaboration with domain experts should cluster around the genuinely complex, differentiating parts of the system.
  • Makes "buy vs. build" decisions principled rather than ad hoc or driven by NIH (not-invented-here) bias.

Cons#

  • Requires genuine business knowledge to classify correctly - an engineering team isolated from domain experts and business strategy cannot reliably judge what is "core" versus merely complicated-looking.
  • The classification is not static: what is generic today (e.g., basic recommendation algorithms) can become a core differentiator tomorrow as a company invests in it, requiring re-classification and re-architecture.
  • Easy to rationalize: teams sometimes label their favorite or most interesting problem "core" to justify over-engineering it, when it is genuinely just complicated, not differentiating.
  • Says nothing yet about how to model the complex parts - it is a prioritization lens, not a modeling technique; the technique comes later (learning-ddd/07, learning-ddd/08).

Alternatives#

  • Uniform technical standards (apply the same architecture everywhere) - simpler to govern and staff, but systematically wastes effort on generic problems and risks under-investing in the differentiators; this is exactly the anti-pattern this lesson warns against.
  • Cost/complexity estimation without a differentiation axis (e.g., pure story-point or cyclomatic-complexity sizing) - identifies "hard" code but not "differentiating" code; a legally mandated but well-documented tax calculation can score as complex without deserving bespoke domain modeling.
  • Wardley Mapping - a complementary strategic tool from outside the DDD literature that maps components by evolution stage (genesis to commodity) and value chain position; it answers a similar "where should we invest" question from a different angle and pairs well with subdomain classification.
  • ddd-evans's original framing (Core Domain vs. Generic Subdomains) - Eric Evans introduced the same core insight in Domain-Driven Design (2003); Khononov's contribution in this book is to sharpen it into a practical, three-way classification (learning-ddd/02) usable earlier in a project's life, before a full model exists.

When to use it#

Apply this thinking at the very start of any new initiative, and revisit it periodically (quarterly or at major roadmap shifts) for existing systems - especially before deciding to invest engineering time in a rewrite, a new microservice, or adopting tactical DDD patterns anywhere. It is equally useful at the scale of a whole company's system landscape and at the scale of a single new feature ("is this feature part of our core differentiation, or is it a generic capability we're only building because no adequate off-the-shelf option exists yet?").

When NOT to use it#

Do not use this as an excuse to permanently starve "supporting" or "generic" areas of any engineering attention - a broken generic subsystem (e.g., a flaky email provider integration) still causes real damage even though it deserves a thin design. The lesson governs design depth, not quality bar or reliability investment. Also avoid treating the classification as a one-time exercise frozen at project kickoff; businesses pivot, and yesterday's generic capability can become tomorrow's core differentiator (a classic example: cloud infrastructure was a generic supporting concern for most companies until it became Amazon's core business via AWS).

Key takeaways / mental model#

Before writing a line of code or picking a pattern, ask two questions about the piece of the business you're building for: (1) How complex is this, really? and (2) Does doing this well differentiate us from competitors, or could any competent team build the same thing the same way? Design investment - modeling depth, team seniority, build-vs-buy - should scale with the combination of those answers, not with either alone.

Self-check questions#

  1. Pick a system you've worked on. Identify one part that is complex but not differentiating, and one part that is differentiating but was treated as if it were simple. What went wrong in each case?
  2. Why is "this is technically hard" not sufficient justification, on its own, for investing in a rich domain model?
  3. Give an example of a capability that used to be "generic" for most companies but became "core" for a specific company that decided to compete on it.
  4. A colleague argues the whole system should use the same layered architecture uniformly "for consistency." Using this lesson's framing, what's the risk in that argument?

References#

  • Learning Domain-Driven Design (Vlad Khononov), Part I, Chapter 1: "What Is Domain-Driven Design?"
  • Domain-Driven Design (Eric Evans, 2003) - original Core Domain framing; see domain-modeling/ddd-evans.