Tactical Design
Inside a bounded context the model describes aggregates in the language of
event storming: operations cause change, events record it, policies react,
and a process is the reaction that remembers.
Everything an aggregate or service offers is a consumable, typed event
or operation; the words command and event stay in prose, not as separate
objects.
What the model leaves out on purpose
Several things a reader expects from Domain-Driven Design are not fields
here, on purpose: this model's own preferences about how a context is drawn,
not consequences of DDD itself, chosen because they keep the maps and the
reaction walk readable and checkable. There is no delivery flag on a
consumable — type is kind, not delivery. There are no coordination fields on
a process — what it correlates on and what it undoes are prose. There are no
modules — a context is the namespace, flat inside it. There are no actors —
who calls an operation is a sentence in its description, not a model
element. There is no read-model element — a projection is a query service
like any other. A value object declares no operations — a value's behaviour
is its invariants and description, not a consumable. An entity has one home
— it belongs to exactly one aggregate of exactly one context, even where two
contexts jointly own it. And a context invariant records who checks it, not
how strongly the store holds it. There is no extension field — an unknown
key is an unknown-field diagnostic and is dropped on save, and comments
live on four seams only: consumables, consumptions, relationships and
processes. Roles are stated on every crossing exchange and again on the
relationship, rather than inferred once, so a pair carries about as many
role declarations on its exchanges as on the relationship itself. And a
partnership shares no shape of its own: two partners that carry each
other's value object or schema declare a shared kernel beside the
partnership. See
decision 15
and the rest of the
decisions/ (each decision opens with a dated current position that states what holds today; read that first, and the amendments below it only for the history)
folder for the reasoning behind each of these, and where a review would find
the argument rather than the absence.
Each of these costs something, and the cost is named rather than hidden:
- The boundary is drawn twice. Every outbound call is an internal operation on the application service that consumes the foreign one, even for a conformist with nothing to translate, so one crossing is two operations and a domain service that wants an outbound port pays for it in one more. The architect's fourteenth round measured it: more than half of every stress model's operations are internal, most of them fronts, against two of sixteen in a clinic model written blind (decision 17).
- An answer stops at the boundary. An operation's answer routes back along
the calling context's own
bychain, through as many local fronts as it takes, and no further: what the neighbour calls next is the neighbour's chain and nothing here has spoken for it, so a process hears the answer to the call its context made and not the one behind it (decisions 21 and 23). - A kernel context loses the pairwise fact. Many contexts sharing a kernel is drawn as a third context they all consume, which gains an honest owner and loses Evans's reading of a kernel as code inside each sharer; two contexts sharing one borrow each other's value objects and schemas directly instead (decision 16).
- Union answers and aggregate timers each wait on a named condition. An operation answers with one shape, so an either-or that is not a refusal has no form yet; and a deadline belongs to a process, so an aggregate that expires on its own clock is watched by a process or by a scheduled operation that raises the expiry. Both reopen on a stated condition rather than on taste (decisions 18 and 23).
- Rules carry no comments. Comments live on the seams — consumables, consumptions and relationships — so an invariant, a policy or an aggregate cannot cite the test that enforces it, and the evidence for a rule is its description (decision 15).
- Translation across a boundary is prose. A consumption records that an
anti-corruption layer exists, with its pattern, its comments and its
by; the map from the upstream's shapes and terms to the downstream's lives in the description, because a mapping table would be the expression language the model refuses (decision 15). identifiesis opt-in, and a denormalised copy is invisible. Nothing forces an author to hold the identity attribute that would record a dependency, and a fact one context copies from another — a seller rating held on an offer — shows nowhere unless the event that carried it is modelled too (decision 14).- A product line's kinds repeat their shared attributes. Specialisation stays inside one aggregate and one context on purpose, so a product line whose kinds sit in separate aggregates cannot inherit, and each aggregate restates the attributes the kinds share (decision 22).
- A version is a second name, and three at once is where that stops working. A changed contract old consumers still need is a second consumable with a name that says so, not a version number, and the model only revisits that once a reference model has to carry more than two versions of one thing at a time (decision 15).
- A relation states one multiplicity, and only the source's. How many of the other side one instance holds is not modelled; where that matters, it is a sentence in the description, not a second cardinality (decision 15).
- Order and timing inside a reaction are prose. Whether a process's waited-for events must all arrive before it acts, or any one is enough, and what starts or clears a deadline's clock, are sentences in its description, not fields the validator reads (decision 23).
raisessays may, not which combination. An operation that raises two events may raise either or both, and the flow map draws every edge the same way; which combination happens on a given call is the operation's description, not the model (decision 15).- A consumption's ref moves. The ref is computed from the pair it joins and only carries a caller's name once a second consumption of the same pair exists, so adding that second consumption changes the first one's ref (decision 26).
- A kernel's co-owners are not listed, and a context keeps one team. The
kernel's
teamnames whoever keeps it, not the sharers who jointly own it, because the model gives every context exactly one team (decision 16). - A lifecycle has no transition table, and a deadline is never a fixed date. A status's values, and the operations that move them, are the author's prose on an invariant, and a deadline is always an interval counted from a named trigger, never a date held in an attribute (decisions 15 and 23).
- Inside a context,
referencesandidentifiesare two forms of one dependency. A relation draws the line and its cardinality and an identity attribute names the key, and an author may write both for the one dependency without the model ever saying they are the same fact (decision 14). - The wire and the model are typed apart. An entity never carries a
schema and a schema never names an entity, so a resource-style API
restates its aggregate as one or more schemas — a
Petbeside aPetSummaryand aRegisterPet— with a shared value object bridging the leaves that repeat (decision 09). - A workspace is one file. Refs never cross files until decision 08's
WorkspaceSetlands; a project that wants several files today keeps each as its own workspace (decision 08). - A refusal enumerates its outcomes and a success does not. A refusal's
reasonsnames each shape a process may wait on and branch across; a success has one edge, and which of several things happened on the way is prose until a reference model needs to branch on it too (decision 18). - A value object may hold an identity into an entity, but no relation to
one.
value-object-shaperefusesincludes,referencesand any relation from a value object to an entity; a value that must point at an entity holds anidentifiesattribute instead, because a value has no identity of its own and draws no line (decision 15). - Calendar-driven behaviour through a Clock costs a relationship and two roles. A context that reacts to the calendar by consuming a Clock context pays for the relationship and the upstream and downstream roles on it; the cheaper route, a scheduled operation that raises the event itself, is the usual one (decision 28).
Value objects
A value object belongs to the bounded context
(context.addValueObject(name)), not to one aggregate: it is part of the
context's ubiquitous language, and every aggregate of the context may hold
one. Another context may borrow one across a shared-kernel relationship, as
a conformist downstream of the context that owns it, or as a downstream of a
customer-supplier relationship, where the negotiated interface's types are
the supplier's published language (decision 16's second amendment of
2026-09-10).
A value provides no operations. A consumable is what a node offers across its own boundary, and a value's behaviour crosses nothing: it is its invariants and its description, not a callable surface of its own.
Attributes
Entities, value objects and schemas carry typed attributes. An
attribute has a free-form type, may be marked identity (for entities),
and may point at the shape that models its type: the value object it is a
value of, or the schema it nests. The two are mutually exclusive — a value
object is a concept of the context's own model, a schema a payload the
context publishes — and only a schema's attribute may name a schema: an
entity or a value object holds value objects, because a payload shape
belongs at the boundary rather than inside the model. A collection stays in
the type string (OrderLine[]), since there is no separate list construct.
An attribute may be marked optional; absent means required, the common
case. An identity attribute is never optional — identity-not-optional
says an identity that may be missing is not an identity.
An attribute that holds an id says whose, with identifies: the entity it is
the identity of. That entity may be in another bounded context, and usually is
— a relation never crosses a boundary, so an identity attribute is the whole of
what one context knows about another's thing. It may also be a child rather
than a root: a playback session holds the id of a profile inside a household,
and the child stays inside its aggregate because its parent's invariants need
it there. You hold the child's id and reach it through its root, so the
dependency is on the aggregate that root leads. identifies keeps that
dependency structural rather than leaving it to a description: the relation map
draws it as a dashed edge to the entity named, inside that entity's own
aggregate cluster, and the identifies-entity rule checks the target is an
entity of this workspace.
Some ids belong to a system whose entities the model does not state: one
nobody here owns (external: true), one of ours nobody can read
(bigBallOfMud: true), or one of ours nobody has interviewed yet
(boundaryOnly: true). A card scheme's authorisation reference, a legacy
account key and a customer id into our own CRM are all of that kind, and
identifies points at the context itself, with the maps drawing the
dependency on it. Where an external or a boundary-only context publishes a
schema for the kind the id names — a processor documents Customer, Payment,
Refund and Dispute as distinct kinds with distinct ids — identifies may name
that schema instead, and the model still reads it as an identity into that
context; a big ball of mud's schemas are not a route, because it publishes no
catalogue of kinds anyone can rely on. Any other context is refused, and so is
a schema of one: there the entity exists, and it is what the id is of.
Specialisation
An entity or value object may be a kind of another
(entity.specialises(other)): it has every attribute and relation of the
one it specialises, plus its own. An entity is a kind of an entity of its
own aggregate; a value object is a kind of one its own context declares, or
one it borrows through a shared kernel, as a conformist, or as a
customer-supplier downstream of the context that owns it. A subtype is never
itself root: true — a
kind of the root is reached through it — and does not redeclare an
attribute it already has from its parent. NorthBank's customer and nominal
ledger accounts, or StreamLine's films and series, are kinds of one ledger
account or one title: one identity scheme and one set of invariants, with
attributes each kind has and the others do not.
Relations and invariants
Entity relations (includes, uses, references) may carry a UML
cardinality: 1, 0..1, * or 1..*. Across aggregates only references
is allowed and it must target the other aggregate's root, or a kind of that
root.
A relation's label is a phrase the relation map draws ("lives at", "in
arrears of"). Where one entity uses the same value object for more than one
attribute — a customer's current address beside its address history — the
relation says which attribute it draws with for, and the validator pairs
each attribute with its relation by that rather than by the phrase.
Invariants list what they constrain: entities, value objects, single attributes, and the consumables of their own aggregate. A rule about a transition — "once sold, a pet does not go back to available" — names the operation that makes the transition, because that is where it is enforced; the operation then reads as the rule it has to uphold. Invariants stay prose; there is no expression language.
An aggregate's lifecycle lives the same way: a status attribute whose
values are the author's own words, with an invariant naming the operations
that move it and what must hold when they do. There is no state machine
element and no transition table — the model states what a status may be and
which operations change it, and the rest of the diagram a reader might
expect is prose on those invariants.
Naming an operation says which operation keeps the rule, and nothing more. A
rule checked before that operation runs and not kept true afterwards — enough
funds at initiation, an entitlement at playback start — says so with
precondition: true, and must name the operation it guards
(precondition-names-operation). A guarantee about what the call answers with
— every returned itinerary meets the requested deadline — is the mirror of it
and says so with postcondition: true
(postcondition-names-operation); the two are exclusive, because one is about
the moment before the call and the other about what comes back. Without either
flag the rule is still true after the operation it names: PostEntry must
produce balanced postings, and the postings stay balanced.
Both may reach the payload the call carries. A precondition may constrain the attributes of the schema its guarded operation takes, and those of what a call that guard — or the front that calls it in the same context — already made comes back with: "approve only if the customer is in good standing" reads a standing another context answered with before this call began, and the shape it came back in is a fact this context holds. It may not name this call's own answer, which does not exist when the check runs, nor the other context's entities, which are never in reach. A postcondition constrains what its operation returns or rejects with, and the request it relates them to. Either follows composition: a rule about the amount of an order line is a rule about the request that holds the lines. No other invariant may name a schema's attribute at all — a rule kept true on every save is a rule about the model, and a transport shape is not the model.
An invariant may instead belong to a value object. A rule that is about a
value alone — an IBAN's mod-97 checksum, a Money's single currency — holds by
construction: a value that breaks it is never made, so no save keeps it and no
operation guards it. It constrains that value object's own attributes and
nothing else (invariant-in-value-object); a rule that reaches for the entity
holding the value is that aggregate's.
An invariant may instead belong to the bounded context rather than to one
aggregate: one open application per customer, one active offer per seller
and SKU, a daily transfer limit are true across instances, or across
aggregates, of a context — no single instance can see the others, so the
rule holds only because something checks it before acting. A context
invariant constrains entities and attributes of any aggregate in the
context and must name at least one operation of the context that checks it
(context-invariant-is-checked); nothing it constrains may reach outside the
context (invariant-in-context). It is always a check, and the model records
who checks it, not how strongly the store holds it: whether a unique index or
a serialisable transaction also keeps the same rule true at the database
layer is not what this says, because the aggregate is the model's only unit
of consistency and a context invariant may not claim to be kept on every save
the way an aggregate's is. A check is made on one side of the call or the
other, and the rule says which with precondition or postcondition: a
quotation service that stores nothing has no aggregate to hold the contract
of its own operation, so the context holds it — the weight is checked
before, the quote against the tariff after. A rule across contexts is a
policy or a process reacting to the other context's events instead: a
wallet's balance and an escrow account's balance in a different context can
never be compared inside one save, so keeping them equal is a policy of one
context that reacts to the other's postings and issues its own reconciling
operation, not an invariant reaching across the boundary.
Schemas
A bounded context declares the payload shapes of its messages once, as
schemas (context.addSchema(name)), the way OpenAPI keeps them under
components/schemas. A schema has attributes like an entity does, and an
attribute may point at a value object or at another schema of the same
context, which is how a payload with a nested shape — an order with lines,
an address inside a customer — is modelled. Schemas belong to the context,
not the workspace, because a payload is part of a context's published
language. Any number of consumables may share one schema; the operation that
raises an event and the event itself commonly do.
On an external context, a schema is also one of the kinds that system
publishes — a processor documents Customer, Payment, Refund and Dispute as
distinct kinds with distinct ids — and identifies may name that schema
directly, which still reads as an identity into that context rather than an
invented entity inside a system we do not own.
Events and operations
An aggregate or service provides consumables and nothing else crosses a
node boundary. An event consumable records that something happened; an
operation consumable is the intention to change something (a command or an
API call) and lists the events it may raise (operation.raises(event)).
Either may carry a schema: the payload the caller sends. Write
schema: { of: shape, many: true } where the request is a list of that shape
rather than one of it, as a bulk create is: a root array and an object holding
an array are different shapes, and only the mark tells them apart.
An operation may also declare a returns schema, the shape the caller gets
back. It is what makes a query modellable — GetPetSummary is asked with a
PetId and answers with a PetSummary — and both schemas belong to the
provider's own context. Leave returns off when the operation answers with
nothing worth naming, which is honest for most commands; an event never
declares one, because a fact announced to whoever is listening has no caller
to answer.
An operation may also list the schemas it rejects with: the shapes it
answers with when it refuses. A declined payment, a transfer over the daily
limit, a reservation the stock will not cover — nothing happened, so none of
these is an event, and a transport error stays outside the model. Each
rejection is a schema of the provider's own context, checked by
schema-context exactly as schema and returns are. Leave rejects off
when the operation always succeeds or refuses without a shape worth naming,
which is honest for most commands; an event never lists one, because a fact
that already happened has nothing left to refuse.
A refusal may name the outcomes the contract enumerates for it, in the
contract's own words: rejects: [{ schema, reasons: ["insufficient_funds", "issuer_unavailable"] }]. One shape with a code in it gave a caller one branch
whatever the code said, and a schema per code would say the contract has
several shapes when it has one. Each reason is an answer a policy or a process
may wait on by itself — operation.rejected(schema, reason) — beside the
shape-level operation.rejected(schema), which hears them all. A reason is a
named outcome, never a condition on data: how the caller decides what to do
about it stays in the code.
A consumable is published by default and carries the upstream pattern it
is offered under. Mark it internal: true when it is raised or handled
inside its own context and never offered to others: an internal operation is
a command only the context's own policies or application services issue, an
internal event is one the outside never sees. Consuming an internal
consumable from another context is a validation error.
type is kind, not delivery: there is no delivery flag, because an event
is a fact and an operation is an intent, whatever carries either one. A
command carried over a queue is still an operation — the caller does not
wait synchronously, but the model records what it is, not how it travels; a
comment on the consumption says so where it matters.
An operation people call through a screen is consumed by nobody in the model, and that is the normal case: most of a system's public surface is exactly that. Who may call it is a sentence in its description, not a field, and a maker-checker rule is an invariant in prose on the operation it guards.
Consumptions
An aggregate or service consumes a consumable
(node.consumes(consumable, { pattern })), which is what draws the
dependency on the consumable map. The consumption may also say what of the
consumer is behind it, and which kind of thing that may be follows the
consumable: on a consumption of an operation, by names the consumer's own
operations that make the call; on a consumption of an event, the policies and
processes of the consumer's context that react to it, because a subscription
is not made but woken (consumption-by-operation and
consumption-by-reactor). A subscription service calls the payment gateway
when it renews, not when it lists entitlements, and by is where that reads.
Leaving
by off means the whole consumer, which is fine only where the consumer
provides exactly one operation, because there is nothing to choose between;
where it provides two or more, or none at all, consumption-by-required asks
which of its operations makes the call — except of an external or
big-ball-of-mud consumer, which is not asked. by is the one causal link the
model has from one operation to the next, so without it the reaction walk and
the flow map stop there and an answer to the call reaches nobody.
Policies
A policy lives on a bounded context and says "on these events, then
these operations" (policy.on(...events).issues(...operations)). The
operations it issues are always this context's own; reaching into another
context to run an operation there is that context acting through someone
else's model instead of through the boundary it published, and
policy-in-context refuses it (decision 17). Reacting is different: on may
name another context's event freely, because subscribing to a published fact
is how contexts integrate. on may also name an answer of an operation this context consumes
(operation.returned(), operation.rejected(schema),
operation.completed()), which means "when that answer comes back": a call is
answered and the reaction is to the answer, not to an event somebody invented
for it. An operation that returns nothing still comes back, and
completed() is that answer — one with no shape, which is the whole of what
the caller of a command learns. An answer is named by the call it comes
back from, never by the shape alone, so two operations refusing with one shared
schema wake only whoever named the call that was made.
The flow map walks from the policies of a context through what they react
to, the operations they issue and the events those raise.
Processes
A process is the reaction that outlives one event
(bc.addProcess(name, { description }).starts(...events).on(...events).issues(...operations).ends(...events)).
A policy is stateless and any-of; a process remembers which of its events
have arrived, so it can wait for two facts before it acts, and it says what
finishes an instance. starts names the event, or the operation of this
context, that creates an instance: a command starts a saga as often as a fact
does — open a claim, submit an application — and a starting command is this
context's own, though a starting event may be a neighbour's. starts, on
and ends may name another context's events, exactly as a policy's on may,
and on and ends may name an answer of an operation this context calls,
which is what the commonest process is made of: it calls, waits, and branches
on what came back. then names operations of the process's own context.
Whether the events named in on must all arrive before the process acts, or
any one of them is enough, is not a flag: like what a process correlates on
and what it compensates, that is prose in its description, on purpose — the
model says a process exists and what it listens to and does, and leaves how
it decides to the code.
A process may also keep its own deadlines, by id: a time limit on one
of its instances, stated the business's own words with after ("30
minutes", "two working days") and, optionally, from, the process's own
starts or on entry the interval counts from — absent means from the
moment the instance began. A deadline behaves as an event the process raises
to itself, so on and ends may wait on or end on it exactly as they do on
any other event, and nothing outside the process may name it. What undoes
the deadline, pausing or clearing the clock, is prose in its description
rather than a field, for the same reason the process's other decisions are:
the model states when a clock starts and how long it runs, not the
conditions that stop it. A deadline is always relative to its trigger, never
a date held in an attribute — an appointment or a policy's fixed expiry is a
scheduled operation that raises it, or a Clock event, not an after written
against a calendar date. An author who finds a policy waiting for a second
event promotes it to a process.
Read models
There is no ReadModel element. A projection is a query service like
any other: a policy of the context reacts to the events that feed the view
and issues an internal operation that writes what the query later reads, and
the query operation itself declares returns the view's own shape.
Petstore's inventory projection is a bounded context of its own because it
serves two subdomains, not because the model forced a construct it does not
have.
Glossary
Each context keeps its ubiquitous language: terms with a definition, optional aliases, and optionally the element that embodies the term. The doc generator produces a glossary page and the UI lists terms on each context.
import { ODSFlowMap, Workspace } from "@open-domain-specification/core";
import { describe, expect, it } from "vitest";
const ws = new Workspace("eCommerce", {
description: "Tactical design example",
version: "0.1.0",
});
const ordering = ws.addBoundedContext("Ordering", { description: "" });
const order = ordering.addAggregate("Order", { description: "A purchase" });
// A value object belongs to the bounded context: every aggregate of the
// context may hold one. Entities and value objects carry typed attributes;
// identity attributes identify an entity and an attribute may point at the
// value object that models its type.
const money = ordering.addValueObject("Money", {
description: "Amount + currency",
});
money.addAttribute("amount", { type: "decimal" });
money.addAttribute("currency", { type: "ISO 4217" });
const orderRoot = order.addRootEntity("Order", { description: "Order header" });
orderRoot.addAttribute("id", { type: "OrderId", identity: true });
orderRoot.addAttribute("total", { type: "Money", valueobject: money });
const line = order.addEntity("Order Line", { description: "One item" });
orderRoot.includes(line, "has lines", "1..*"); // relations may carry a cardinality
// Invariants say what they constrain.
const nonEmpty = order
.addInvariant("Non-empty", { description: "An order has at least one line" })
.constrains(line);
// A context declares the payload shapes of its messages once, as schemas, and
// a schema may name another as the type of one of its attributes.
const lineSummary = ordering.addSchema("Line Summary", {
description: "One line, as the outside world sees it",
});
lineSummary.addAttribute("sku", { type: "SKU", identity: true });
const orderSummary = ordering.addSchema("Order Summary", {
description: "What the outside world learns about an order",
});
orderSummary.addAttribute("orderId", { type: "OrderId", identity: true });
orderSummary.addAttribute("total", { type: "Money", valueobject: money });
orderSummary.addAttribute("lines", {
type: "LineSummary[]",
schema: lineSummary,
});
// An aggregate provides event consumables; an operation consumable lists the
// events it raises. Both may point at a schema.
const placed = order.provides("Order Placed", {
description: "An order was placed",
type: "event",
pattern: "published-language",
schema: orderSummary,
});
ordering
.addService("Checkout", { description: "", type: "application" })
.provides("Place Order", {
description: "POST /orders",
type: "operation",
pattern: "open-host-service",
schema: orderSummary,
})
.raises(placed);
// An internal consumable never leaves its context.
const lineAdded = order.provides("Line Added", {
description: "",
type: "event",
internal: true,
});
// A transition rule is about the operation that makes the transition, so the
// invariant names it: only the aggregate's own consumables may be named.
const removeLine = order.provides("Remove Line", {
description: "Takes a line off the order",
type: "operation",
internal: true,
});
nonEmpty.constrains(removeLine);
// Policies react to events with operations, even across contexts.
const billing = ws.addBoundedContext("Billing", { description: "" });
const invoice = billing.addAggregate("Invoice", { description: "" });
const raise = invoice.provides("Raise Invoice", {
description: "",
type: "operation",
internal: true,
});
const invoiceOnPlaced = billing
.addPolicy("Invoice on order placed", { description: "" })
.on(placed)
.issues(raise);
// A consumption is what draws the dependency between two nodes. `by` says
// what of the consumer makes it: here only the policy reads the other
// context's event. Left off, it means the whole consumer.
invoice.consumes(placed, { pattern: "conformist", by: [invoiceOnPlaced] });
// Each context keeps its ubiquitous language.
ordering.addTerm("Order", {
definition: "A customer's request to buy items",
aliases: ["Purchase"],
embodiedBy: order,
});
describe("Tactical design", () => {
it("walks the flow from event to policy to operation", () => {
const edges = Array.from(
ODSFlowMap.fromBoundedContext(billing).edges.values(),
);
expect(
edges.map((e) => `${e.source.name} -> ${e.target.name}`),
).toMatchInlineSnapshot(`
[
"Order Placed -> Invoice on order placed",
"Invoice on order placed -> Raise Invoice",
]
`);
});
it("serialises consumables, schemas and the glossary", () => {
const schema = ws.toSchema();
expect(
schema.boundedcontexts.ordering.aggregates.order.provides.order_placed,
).toMatchInlineSnapshot(`
{
"comments": undefined,
"description": "An order was placed",
"disposition": undefined,
"internal": undefined,
"name": "Order Placed",
"pattern": "published-language",
"raises": undefined,
"rejects": undefined,
"returns": undefined,
"schema": {
"$ref": "#/boundedcontexts/ordering/schemas/order_summary",
"many": undefined,
},
"type": "event",
}
`);
expect(
schema.boundedcontexts.ordering.services.checkout.provides.place_order
.raises,
).toMatchInlineSnapshot(`
[
{
"$ref": "#/boundedcontexts/ordering/aggregates/order/provides/order_placed",
},
]
`);
expect(
schema.boundedcontexts.ordering.schemas.order_summary.attributes.total,
).toMatchInlineSnapshot(`
{
"description": undefined,
"identifies": undefined,
"identity": undefined,
"name": "total",
"optional": undefined,
"schema": undefined,
"type": "Money",
"valueobject": {
"$ref": "#/boundedcontexts/ordering/valueobjects/money",
},
}
`);
expect(
schema.boundedcontexts.ordering.schemas.order_summary.attributes.lines,
).toMatchInlineSnapshot(`
{
"description": undefined,
"identifies": undefined,
"identity": undefined,
"name": "lines",
"optional": undefined,
"schema": {
"$ref": "#/boundedcontexts/ordering/schemas/line_summary",
},
"type": "LineSummary[]",
"valueobject": undefined,
}
`);
expect(
schema.boundedcontexts.ordering.aggregates.order.invariants.non_empty
.constrains,
).toMatchInlineSnapshot(`
[
{
"$ref": "#/boundedcontexts/ordering/aggregates/order/entities/order_line",
},
{
"$ref": "#/boundedcontexts/ordering/aggregates/order/provides/remove_line",
},
]
`);
expect(lineAdded.internal).toBe(true);
expect(
schema.boundedcontexts.ordering.glossary.order.embodiedBy?.$ref,
).toBe("#/boundedcontexts/ordering/aggregates/order");
});
});