Case file — A6

Bounded Contexts

One context, one model, one folder.

No single domain model is universally correct across a large system. Each context owns its own model, its own folder, and its own meaning of “Customer.” Cross-context use goes through translation, not through reuse.

ByAdam LewisPublished3 May 2026Reading12 minVersionv1.0ConfidenceHigh
§0b

Opinion

I've spent enough time inside DDD-curious codebases to know what happens when the design half of the rule ships without the coding half. The team reads Domain-Driven Design, agrees that “Customer” means three different things in three different parts of the system, and then ships a single User type used everywhere. Six months later the type has thirty-seven fields, half of them nullable, and a function called denormalizeForBilling that is the Anti-Corruption Layer in embryo without the name.1Eric EvansDomain-Driven Design (Addison-Wesley, 2003), Part IV “Strategic Design,” Ch. 14 “Maintaining Model Integrity.” The chapter that introduced Bounded Context, Context Map, Anti-Corruption Layer, Shared Kernel, Open Host Service, and Published Language as integration patterns. The boundary that wasn't enforced isn't a boundary.

The lever that makes Bounded Contexts a coding rule is the import graph. Each context is a top-level folder; cross-context imports are banned by lint; integration happens through an ACL adapter that lives at the edge. Once the rule is mechanical the language stays clean by construction. A1 Screaming Architecture is the surface; Bounded Contexts is the model behind the surface. Without the model, A1 produces a folder tree that screams the right words; without A1, Bounded Contexts has no folder to enforce on.

The reason DDD newcomers struggle with this rule is not the concept; it is the verb. “Respect the context” sounds like a worldview. “Lint the cross-context import” is a Tuesday-afternoon task. The dossier carries both because the rule needs both: the design-level framing for why the boundary exists, and the lintable framing for how the boundary stays where it was set.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

One context, one model, one folder. Each bounded context owns its own `User`, `Order`, `Customer` — never shared across folders. Cross-context use goes through a single Anti-Corruption Layer per pair of contexts; nowhere else does the translation. The folder boundary is enforced by `import/no-restricted-paths`.

/tenet/bounded-contexts/A6
§0c

AI eyes only

Rule: one context, one model, one folder. Same word in two contexts is two types.

Reject: a shared User type imported across billing/, auth/, and support/. Reject: cross-context imports of internal types. Reject: collapsing two contexts because the shape looks similar today.

Generate: per-context model. When two contexts must talk, write an explicit ACL function that translates between them. The translation lives at the seam, not in either model.

Diagnostic: identical names across contexts must have explicit translation functions, not direct field access. A field-by-field copy from one context to another is the seam.

§0d

Why?

  • Each context keeps its own model integrity. The word “Customer” means the same thing inside Marketing's folder every time, and a different thing inside Billing's folder every time, and the team never has to pause a meeting to ask which Customer they're talking about.
  • Cross-context imports become a build failure, not a code-review fight. The folder boundary is enforced by import/no-restricted-paths; the design rule and the lint rule are the same rule.
  • Integration happens through one named Anti-Corruption Layer per pair of contexts, not through ad-hoc translation scattered through the codebase. The translation is auditable; when one side changes shape, the other side's ACL is the only file that needs editing.
  • Caps the fan-out of a model change. A change inside Marketing's context recompiles Marketing's folder and one ACL; the rest of the system isn't even touched. The deploy gets smaller; the blast radius gets smaller.
  • Pairs cleanly with A1 Screaming Architecture: A1 is the folder surface; Bounded Contexts is the model behind the surface. Together they enforce one model per context with both the file system and the import graph.
  • Pairs cleanly with T1 Domain-Driven Types: each context owns its own types; cross-context translation happens through the ACL, not through structural compatibility. The compiler enforces what the import lint enforces.
  • Coding agents collapse synonyms by default — the training data is full of shared User types. The cross-context lint forces the agent to translate instead of reuse, which is the discipline the rule is here to teach.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

Eric Evans introduced Bounded Context in Domain-Driven Design (Addison-Wesley, 2003), Part IV “Strategic Design,” Ch. 14 “Maintaining Model Integrity.”1Eric EvansDomain-Driven Design (Addison-Wesley, 2003), Part IV “Strategic Design,” Ch. 14 “Maintaining Model Integrity.” The chapter that introduced Bounded Context, Context Map, Anti-Corruption Layer, Shared Kernel, Open Host Service, and Published Language as integration patterns. The chapter's opening claim: no single domain model is universally correct across a large system. The book introduces Bounded Context as the explicit boundary inside which one definition applies, and Context Map as the diagram of how multiple bounded contexts integrate — through Shared Kernels, Anti-Corruption Layers, Open Host Services, and Published Languages. The Anti-Corruption Layer is the rule's lintable half: cross-context integration goes through one named adapter, never through ad-hoc translation logic scattered through the codebase.

Vaughn Vernon's Implementing Domain-Driven Design (Addison-Wesley, 2013) is the practitioner companion that turned Evans's strategic-design vocabulary into concrete code patterns.5Vaughn VernonImplementing Domain-Driven Design (Addison-Wesley, 2013), Ch. 2 “Domains, Subdomains, and Bounded Contexts” and Ch. 3 “Context Maps.” The practitioner companion that turned Evans's strategic-design vocabulary into concrete code patterns. Vernon's Ch. 2 “Domains, Subdomains, and Bounded Contexts” is the canonical readable introduction; Ch. 3 “Context Maps” walks the integration patterns into worked examples. Vernon's contribution to the lintable half: the ubiquitous language for a context lives inside that context's code; types are not shared across contexts; integration happens through a single named adapter.

Vlad Khononov's Learning Domain-Driven Design (O'Reilly, 2021) is the modern restatement.4Vlad KhononovLearning Domain-Driven Design (O'Reilly, 2021), Ch. 4 “Integrating Bounded Contexts.” The integration-pattern chapter sharpened into a decision tree — ACL, Open Host Service, Shared Kernel, when to reach for which. Khononov pairs the strategic-design vocabulary with subdomain types (core, supporting, generic) so that each top-level folder carries a meaning inherited from strategic design, not just from feature naming. Khononov's Ch. 4 “Integrating Bounded Contexts” sharpens the integration patterns — ACL, Open Host Service, Shared Kernel — into a decision tree the team can run on a Tuesday afternoon.

Martin Fowler's BoundedContext bliki entry is the single best one-paragraph summary in print.6Martin Fowler“BoundedContext” (bliki). The single best one-paragraph summary in print. Frames the rule as the answer to “Total unification of the domain model for a large system will not be feasible or cost-effective.” Fowler frames the rule as the answer to the question “Total unification of the domain model for a large system will not be feasible or cost-effective” and points at the integration patterns from Evans. Useful for the team meeting where one person hasn't read the book; the bliki entry is short enough to skim and concrete enough to argue with.

§2

Quotes

Explicitly define the context within which a model applies. Explicitly set boundaries in terms of team organisation, usage within specific parts of the application, and physical manifestations such as code bases and database schemas.

Eric Evans · Domain-Driven Design (2003), Ch. 14

Total unification of the domain model for a large system will not be feasible or cost-effective. So divide the model into Bounded Contexts and be explicit about their interrelationships.

Martin Fowler · BoundedContext (bliki)

A Bounded Context is an explicit boundary within which a domain model exists. Inside the boundary, all terms and phrases of the Ubiquitous Language have specific meaning.

Vaughn Vernon · Implementing Domain-Driven Design (2013)

Bounded contexts are physical boundaries. They are implemented as separate codebases, owned by a single team, evolved at their own pace.

Vlad Khononov · Learning Domain-Driven Design (2021)
§3

Evidence

Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Eric Evans · 2003
    The chapter that introduced Bounded Context. Names the boundary, the Context Map, and the integration patterns (Shared Kernel, ACL, Open Host Service, Published Language). The single canonical source.
  2. 02
    Eric Evans · 2015
    The free PDF reference Evans published as the abbreviated guide to the 2003 book. Useful for the team meeting where someone hasn't read the full book — short enough to skim, precise enough to argue with.
  3. 03
    Vaughn Vernon · 2013
    Practitioner companion to Evans. Ch. 2 “Domains, Subdomains, and Bounded Contexts” is the canonical readable introduction; Ch. 3 “Context Maps” walks the integration patterns into worked examples.
  4. 04
    Vlad Khononov · 2021
    Modern restatement that pairs strategic design with subdomain types (core, supporting, generic). Each context inherits a meaning from strategic design, not just from feature naming. The most up-to-date reading.
  5. 05
    Vlad Khononov · 2021
    The integration-pattern chapter sharpened into a decision tree the team can run on a Tuesday afternoon. ACL, Open Host Service, Shared Kernel — when to reach for which.

Sixteen sources, three stances. The supporters cluster on the strategic-design canon: Evans' foundational chapter and DDD Reference, Vernon's implementation companion, Khononov's modern restatement. The qualifiers further down carry the harder reading: the context boundary is discovered by collaboration, not declared by a folder, and a folder structure built before the boundary is found will get redrawn. The opposers carry the steelman: a single shared model is simpler, and the cost of translation is real.

§4

Examples

Viewing: TypeScript.
Avoid
Fileanalytics/build-sighting-report.ts
// Before: analytics imports tracking.Hedgehog and reaches into its lifecycle.import { Hedgehog } from "@/tracking/hedgehog.types";export function buildSightingReport(hedgehogs: Hedgehog[]) {  return hedgehogs.filter((h) => h.lifecycleState === TRACKING_INTERNAL_ACTIVE).map((h) => ({    id: h.microchipId, area: h.gpsTrack[0].areaName, seenAt: h.lastSeenAt,  }));}
Prefer
Fileanalytics/build-sighting-report.ts
// After: analytics owns its own type. An ACL maps tracking.Hedgehog at the seam.import { fromTrackingHedgehog } from "./anti-corruption/from-tracking";import type { AnalyticsHedgehog } from "./analytics-hedgehog.types";import type { Hedgehog as TrackingHedgehog } from "@/tracking/hedgehog.types";export function buildSightingReport(tracked: TrackingHedgehog[]): AnalyticsHedgehog[] {  return tracked.map(fromTrackingHedgehog);}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
import/no-restricted-pathseslint-plugin-importany import that crosses a context boundary directly — marketing reaching into billing's domain code without going through marketing/integration/ first.
no-restricted-importsESLint corecross-context type imports — the import shape that says two contexts are sharing a model rather than translating between them.
import/no-cycleeslint-plugin-importimport cycles between contexts — the strongest signal that two contexts have grown into each other rather than past each other; a candidate for merging or for a Shared Kernel.
Context Mapper DSLContext Mapperdesign-time validation of the context map itself — useful when the team has a context-map artefact that should drive the folder structure rather than follow it.
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';

export default tseslint.config({
  files: ['**/*.{ts,tsx}'],
  plugins: { import: importPlugin },
  rules: {
    'import/no-restricted-paths': ['error', {
      zones: [
        {
          target: './src/marketing',
          from: ['./src/billing', './src/support'],
          message: 'cross-context import — go through the ACL in marketing/integration/, not direct.',
        },
        {
          target: './src/billing',
          from: ['./src/marketing', './src/support'],
          message: 'cross-context import — go through the ACL in billing/integration/, not direct.',
        },
        {
          target: './src/support',
          from: ['./src/marketing', './src/billing'],
          message: 'cross-context import — go through the ACL in support/integration/, not direct.',
        },
      ],
    }],
    'no-restricted-imports': ['error', {
      patterns: [
        {
          group: ['@/marketing/types', '@/billing/types', '@/support/types'],
          message: 'never share types across contexts — translate via the ACL adapter.',
        },
      ],
    }],
  }
});
§4c

AI rules

File.cursor/rules/a6-bounded-contexts.mdc
---
description: Prickles A6 — Bounded Contexts
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---

## Prickles A6 — Bounded Contexts

One context, one model, one folder. Each bounded context is a top-level folder; cross-context imports are linted, not encouraged.

No shared types across contexts. Each context owns its own `User`, `Order`, `Customer` — the same word may mean different things in different folders, and that's the design.

Cross-context use goes through one named Anti-Corruption Layer per pair of contexts. Translation lives in the ACL adapter; nowhere else does the translation.

Refuse to reach across a context boundary directly. If you would, write the ACL or move the type into the calling context's vocabulary.

Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.

§5

Counter-argument

Counter

The strongest steelman is the single-model camp combined with the EventStorming critique.2Sam NewmanBuilding Microservices, 2nd ed. (O'Reilly, 2021). The qualifier on bounded contexts as service boundaries — the cost of translation is real, and the single-shared-model camp has a defensible argument when the team is small enough. A single shared model is simpler than three contexts that translate between themselves. Translation is overhead; the ACL adapter is a class that exists only to convert one shape into another, and a sufficiently well-modelled Customer type could cover all three meanings with optional fields and discriminated unions. Brandolini and the EventStorming camp sharpen the same point: the right context boundary is discovered through collaborative modelling sessions, not declared in a folder structure on day one. Drawing folders before the boundary is found commits the team to a structure the next requirement will redraw, and the lint rule that enforced the structure becomes the rule the team has to fight to make progress.

§6

Counter-argument retort

Reply

The single-model argument is the one I've heard most often, and the reply is that it depends on what you mean by “simpler.”2Sam NewmanBuilding Microservices, 2nd ed. (O'Reilly, 2021). The qualifier on bounded contexts as service boundaries — the cost of translation is real, and the single-shared-model camp has a defensible argument when the team is small enough. A single shared model is simpler to read on day one and harder to evolve from day two onwards. A Customer type that covers Marketing's, Billing's and Support's meanings has thirty-seven fields and ten discriminated unions; a change to Marketing's view forces a recompile of Billing and Support and an audit of every caller. Three contexts with explicit translation between them ship that same change as one file edit inside Marketing's folder, with no fan-out. The cost of translation is real — and so is the cost of every cross-team meeting where Marketing argues with Billing about what a field on the shared type should do. Pick the cost you want to pay.

The Brandolini critique — that boundaries are discovered, not declared — is correct as far as it goes, and the rule respects it.3Alberto BrandoliniEventStorming. The collaborative-modelling technique that discovers context boundaries; the rule is that boundaries are discovered, not declared. The folder structure is output, not input. Bounded Contexts doesn't require you to draw the right folder structure on day one; it requires you to draw some folder structure on day one and to maintain the discipline of redrawing it as the boundaries get discovered. EventStorming sessions and the resulting context map are inputs to the folder structure; the folder structure is output. When the next session moves a boundary, the folder moves with it. The rule that lints cross-context imports doesn't fossilise the boundary — it makes moving the boundary visible.

The novelty challenge — that this is just “feature folders” or “modular monolith” under a different name — misses the model-integrity half. Feature folders give you a shape; Bounded Contexts give you the rule that the language inside the shape is consistent and the translation across the shape is explicit. Without the context boundary, “feature folders” produces a tree where two folders import each other's User types and quietly drift into incompatibility — which is a tree that scream-tests fine and still produces every problem the rule was here to prevent.

The genuine residue is the granularity question. Bounded Contexts doesn't tell you how big a context should be; that's a strategic-design call the EventStorming session is meant to settle.4Vlad KhononovLearning Domain-Driven Design (O'Reilly, 2021), Ch. 4 “Integrating Bounded Contexts.” The integration-pattern chapter sharpened into a decision tree — ACL, Open Host Service, Shared Kernel, when to reach for which. Pair this with A4 Common Closure for the package-grouping rule that complements it: a context is the natural unit of common closure, and CCP's axis-of-change usually converges on the context boundary.

§7

Notes

  1. [1]Eric EvansDomain-Driven Design (Addison-Wesley, 2003), Part IV “Strategic Design,” Ch. 14 “Maintaining Model Integrity.” The chapter that introduced Bounded Context, Context Map, Anti-Corruption Layer, Shared Kernel, Open Host Service, and Published Language as integration patterns.
  2. [2]Sam NewmanBuilding Microservices, 2nd ed. (O'Reilly, 2021). The qualifier on bounded contexts as service boundaries — the cost of translation is real, and the single-shared-model camp has a defensible argument when the team is small enough.
  3. [3]Alberto BrandoliniEventStorming. The collaborative-modelling technique that discovers context boundaries; the rule is that boundaries are discovered, not declared. The folder structure is output, not input.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27