# Domain-Driven Design
**Sample code** — the actual source files this note examines, in the public
ngx-experiments repo:
- **Domain model** — transaction.model.ts
- **Domain tokens** — _ldg-money-tokens.scss
- **Model barrel** — ledger-models
Domain-Driven Design asks the business domain — not the framework, the database, or the UI —
to drive the software model. On a large front-end that principle carries a concrete
architectural consequence: code organizes around what the product *does*, and a single domain
owns its models, its state, its presentation, and even its palette as one self-contained unit.
Everything below is grounded in one real, public domain — a personal-finance ledger (the `ldg-`
prefix, the `@nge/ledger-*` libraries) built on a shared foundation — because an abstraction is
easiest to trust when each one points at a file.
## Bounded contexts
A bounded context is the boundary within which a model and its language stay consistent. In
this workspace that boundary is drawn as a **domain**, and a domain is a set of libraries rather
than a folder of loosely related files. The ledger owns six:
```
libs/ledger/
├── models # the domain's types — Account, Transaction, Budget, Category, Bill
├── utils # its pure functions — spendingByCategory, cashflow, netWorthSeries
├── store # its state
├── ui # its smart, container components
├── design-library # its presentational components (ldg- prefix)
└── themes # its tokens (--ldg-*)
```
The boundary is stated three times over: the library set that encloses the domain, the
`@nge/ledger-*` import path that names it, and the `ldg-` prefix that marks everything belonging
to it. Two contexts may use one word for two things — a *balance* under a budgeting model is not
the *balance* an accounting-reconciliation model would compute — and that divergence stays safe
precisely because each context is sealed behind its own seam. Nothing outside the ledger reaches
past that boundary to bind itself to the domain's internals; a consumer depends on the published
import path, never on a file buried inside it.
## Ubiquitous language
The names in the code should be the names the domain speaks, and the discipline is to let that
language reach everywhere — into the types, through every function that reads them, and, less
obviously, into the design tokens.
The clearest instance is how the ledger models money. A `Transaction` carries no `type` field at
all:
```ts
export interface Transaction {
accountId: string;
/** Signed integer minor units (cents). Negative = outflow, positive = inflow. */
amountCents: number;
categoryId: string;
// …
}
```
The sign of `amountCents` *is* the type: a negative amount is an expense, a positive amount is
income, and every aggregation in `@nge/ledger-utils` — `spendingByCategory`, `cashflow`,
`budgetVsActual`, `netWorthSeries` — keys off that one convention rather than inventing its own.
Money is always an integer count of minor units (cents), never a float and never a formatted
string, and it is formatted only at the presentation edge. A single sentence of domain language —
*the sign is the type, money is cents* — therefore propagates unbroken from the model through
every calculation that touches it.
That language does not stop at TypeScript. A domain's vocabulary also becomes color. The ledger
themes define what money *means* as a token — `--ldg-money-positive` for a credit,
`--ldg-money-negative` for a debit — held deliberately apart from the generic `--ldg-error` and
`--ldg-warning` states, because a debit is not a failed form field. The token layer even records
that a spending-category hue, once shipped, is never reassigned, so no historical chart silently
relabels itself. The full mechanics of a domain encoding its language as tokens are the subject
of the [design-library note](https://jerryorta.com/concepts/design-library-architecture) under "Tokens speak the
domain's language"; the point here is narrower — the language is one thing, expressed in two
materials, and it says the same thing in both.
## Self-contained domains as a direction
If a bounded context is the idea, a self-contained domain is the direction the workspace takes to
honor it. The working rule is to prefer a domain's own libraries and to reserve the shared
foundation for what is genuinely generic — a primitive button, a token contract, a charting
engine no single domain should own.
Self-contained does not mean sealed off. A domain builds on the shared foundation freely —
importing its primitives, inheriting its tokens — because the dependency runs in one safe
direction: a domain draws downward on shared infrastructure, never sideways into a sibling
domain, and the shared foundation never reaches back up. What a boundary forbids is coupling
between sibling domains, not use of the shared floor they all stand on.
The ledger shows both sides of that balance. Its own presentational pieces — an account card, a
budget card — live in `libs/ledger/design-library` under the `ldg-` prefix, and they compose the
generic primitives such as `dlc-button` and `dlc-card` from the shared `@nge/ui-design-library`
instead of reimplementing them. Its tokens make the same move: most `--ldg-*` values are pure
passthrough aliases of the shared `--dlc-*` contract.
```scss
@mixin ldg-aliases() {
--ldg-surface: var(--dlc-surface, #f8f9fa);
--ldg-primary: var(--dlc-primary, #0f2b3c);
// …
}
```
Custom-property indirection resolves each alias against whichever shared persona is active on an
ancestor, so the domain inherits the shared look and writes fresh values only for what is truly
its own — the money and category tokens above. A domain defines what only it can define, and it
inherits everything else.
That preference for domain-owned code is also why placement is a deliberate act rather than a
reflex. Before a type, a utility, or a component is written, the first question is whether the
domain or the shared foundation already provides it, and the second is where a genuinely new
piece belongs: a pure model in `models`, a stateless function in `utils`, a presentational
component in the domain's `design-library`. Searching before writing is not a tidiness rule; it
is the practical expression of a context boundary. Duplication is how a boundary quietly erodes —
one convenient copy at a time — until two contexts that were meant to stay separate share a model
that neither of them fully owns.
## Why it holds together
Three ideas, one shape:
- **A bounded context is a domain** — a set of libraries (`models`, `utils`, `store`, `ui`,
`design-library`, `themes`) sealed behind an import path and a prefix, so two contexts can hold
different meanings for one word without collision.
- **A ubiquitous language reaches everywhere** — the domain's vocabulary lives in its type names,
travels through every function that reads them, and surfaces again in its tokens; *the sign is
the type, money is cents* holds from the model all the way to the palette.
- **Self-contained domains are the direction** — a domain owns its libraries and builds on the
shared foundation for the generic, defining only its deltas and inheriting the rest, with
placement settled by searching before writing.
The dividend is a dependency graph that stays legible as the workspace grows: each domain is a
unit one can reason about whole, and the lines between domains are written down as import paths
rather than left to discipline. The same instinct — a self-contained unit that owns everything it
needs — reappears one layer out, in how I arrange the repository's [instruction files for an AI
agent](https://jerryorta.com/concepts/claude-agent-files), and one layer in, in how a
[themeable component library](https://jerryorta.com/concepts/design-library-architecture) and a
[charts library](https://jerryorta.com/concepts/charts-library-architecture) each keep their structure and their
values on opposite sides of a single contract. A domain is that same discipline turned on the
product itself.