Everyone in the eligibility platform uses the word eligible. Enrollment means a person may join a plan on a date. Underwriting means enough evidence exists to accept a risk. Fulfillment means an approved benefit can be delivered. Compliance means the decision followed the rules in force and can be explained later.
The shared Eligibility table has one status column. Teams add flags to qualify it: is_provisional, is_fulfillable, is_underwriting_complete, needs_compliance_review. A change for one workflow breaks another because the software treats a family of related concepts as a single universal fact.
Domain-Driven Design starts by letting the business reveal where one model stops working. The goal is not to reproduce the organization chart or create a microservice for every noun. It is to build a model that makes important business decisions explicit within the context where its language is valid.
Language is part of the architecture
A Ubiquitous Language is not a glossary produced once by architects. It is the language domain experts and developers use in conversation, examples, tests, and code. If an enrollment specialist says “coverage effective date,” a model named activationTimestamp may be hiding an important distinction. If underwriting distinguishes “insufficient evidence” from “ineligible,” one Boolean cannot preserve the decision.
// Enrollment context
type EnrollmentDecision =
| Enrollable { effectiveOn: Date }
| NotEnrollable { reasons: EnrollmentReason[] }
// Underwriting context
type Assessment =
| AcceptedRisk { class: RiskClass }
| MoreEvidenceRequired { requests: EvidenceRequest[] }
| DeclinedRisk { reasons: UnderwritingReason[] }
These types are not duplication to eliminate. They state that enrollment and underwriting answer different questions. A generic EligibilityStatus would save symbols by discarding meaning.
The model improves through collaboration: examples that fail, terms that experts dispute, and rules that resist the current abstraction. The software should make those disagreements visible while the change is still a conversation, not after it becomes an incident.
One model does not have to rule the enterprise
Eric Evans introduced the Bounded Context because large systems cannot maintain one coherent model of everything. A model is valid inside a boundary with a particular purpose, language, and set of rules.
The eligibility platform might contain:
- Enrollment: whether and when a person can enter a plan;
- Underwriting: whether evidence supports accepting a risk;
- Fulfillment: whether an approved benefit can be delivered;
- Compliance: whether a decision is explainable under a policy version.
The boundary is conceptual before it is deployable. Two contexts can live in one modular monolith. One context can span several processes if operations require it. A Bounded Context is not synonymous with a microservice, team, database, or namespace, even though those boundaries may eventually align.
Translate instead of pretending
Contexts still exchange information. They should do so through an explicit relationship rather than a shared object that gradually accumulates every meaning.
type UnderwritingOutcome =
| RiskAccepted { assessmentId: Id, class: RiskClass }
| RiskDeclined { assessmentId: Id, codes: ExternalReasonCode[] }
function toEnrollmentEvidence(outcome: UnderwritingOutcome): EnrollmentEvidence {
match outcome {
RiskAccepted(id, _) => VerifiedEvidence(reference = id)
RiskDeclined(id, codes) => UnmetRequirement(
reference = id,
reasons = translateUnderwritingCodes(codes)
)
}
}
That translation is an anti-corruption boundary: Enrollment does not adopt Underwriting's internal model, and Underwriting does not pretend to issue enrollment decisions. If the contexts disagree about a term, the mapping records the disagreement instead of erasing it.
A context map names the relationship and the direction of influence. Does one context conform to another? Do they share a published language? Does an anti-corruption layer protect one side? The map helps teams decide where coordination is unavoidable and where translation can contain it.
Spend modeling effort on the core domain
DDD also asks where custom modeling creates business advantage. A core domain contains complexity important to the organization's differentiation or risk. Supporting subdomains are necessary but not distinctive. Generic subdomains may be better served by established products or straightforward implementations.
In this platform, explainable, versioned eligibility policy may be core. Sending email is probably generic. Building a rich domain model for SMTP configuration consumes the same developer attention the series is trying to return.
Strategic DDD—language, subdomains, contexts, and their relationships—often creates value without adopting every tactical pattern. Entities, value objects, aggregates, repositories, and domain services are tools, not a required checklist. Use them when they clarify invariants and change, not to make a directory look domain-driven.
What it buys
DDD can make business rules easier to find because the code uses the language of the work. Bounded Contexts can contain semantic change: Underwriting can refine risk assessment without redefining Enrollment's effective-date rules. Explicit translations can prevent a shared status from carrying incompatible assumptions across teams.
The possible quality-of-life improvement is less translation in developers' heads. Fewer meetings are spent discovering that two teams mean different things by the same word. Reviews can include domain experts because types and examples reflect business decisions. Incidents can be located in the context that owns the meaning.
What it costs
Good modeling requires sustained access to domain experts and willingness to revisit language. Context boundaries need stewardship. Translators and published contracts must evolve. A model can become a private dialect that the business does not recognize. Teams can spend months cataloging nouns while delivering no safer change.
Common failure modes include:
- treating DDD as a package layout or collection of tactical classes;
- inventing “domain language” without the people who perform the work;
- creating an aggregate around every database table;
- equating every Bounded Context with a microservice;
- copying one enterprise model across contexts to avoid translation;
- modeling generic CRUD as if it were a source of strategic complexity;
- preserving a context map after organizational and product boundaries have changed.
When not to use it
Do not install tactical DDD around simple CRUD, generic administration, or a disposable experiment. If the business rules are shallow and the data model already communicates them clearly, a rich domain layer may add ceremony without protecting change. If domain experts are unavailable, a supposedly authoritative model can fossilize developer guesses.
The smallest viable adoption is language work: name one important decision in the terms the business uses, capture representative examples, and put the rule in one cohesive module. A context boundary should emerge when conflicting meanings or ownership make translation cheaper than continued ambiguity.
Sources
- Eric Evans, Domain-Driven Design Reference, for Ubiquitous Language, subdomains, Bounded Contexts, and context relationships.
- Martin Fowler, Bounded Context, for why multiple models are necessary in large systems.
- Microsoft Azure Architecture Center, Use domain analysis to model microservices, used here for domain-analysis guidance rather than as a claim that DDD requires microservices.
- Microsoft Azure Architecture Center, Anti-Corruption Layer pattern, for translating between systems with different semantics.
- Nicole Forsgren and colleagues, The SPACE of Developer Productivity, for treating satisfaction, communication, and flow as part of developer experience.