Interfaces, Modules, and Operations are the exposure and decomposition machinery inside a bounded context. An interface selects which operations are visible to consumers — it is the surface through which the outside reaches in. A module groups related concepts behind one or more such interfaces, encapsulating everything else. An operation is the atomic unit of behavior — safe or unsafe, idempotent or not, with typed arguments, a typed result, and possibly emitted events. Together they define how the domain is packaged and how the system communicates with the outside world.
Interface
An Interface is a named surface that exposes a subset of operations from entities or domain services. An interface is a selector, not an owner: the operations themselves remain owned by their entity or domain service. An interface lists which of those operations are visible to consumers and how they are named at the boundary.
An interface belongs to exactly one module. It exposes 1..n operations and may draw those operations from multiple entities or domain services.
Example:
interface OrderPlacementAPI {
satisfies [REQ-ORD-010]
exposes Order.place
exposes Order.cancel
exposes PricingService.quote
}
Module
A Module is a unit of decomposition that groups related domain concepts behind one or more controlled interfaces. It encapsulates its internals and exposes only those interfaces; everything else stays hidden. A module should always be preferred over a bounded context for pure decomposition of concerns — a bounded context breaks the language, a module does not.
A module belongs to exactly one bounded context. It exposes zero or more interfaces and may depend on other modules.
Example:
module placement {
satisfies [REQ-ORD-010]
exposes OrderPlacementAPI
depends on pricing
depends on inventory
}
Operation
An Operation is a named unit of behavior with typed input arguments and one typed output (which may be an error). Every operation has four properties that the modeler must state explicitly:
- Safe or unsafe. A safe operation does not mutate domain state (read-only). An unsafe operation mutates it.
- Idempotent or not. An idempotent operation, invoked twice with the same arguments, yields the same end state as invoking it once.
- Events emitted. An operation may emit multiple events, including error events — which must be declared explicitly, not raised implicitly as exceptions.
- Ownership. Ownership follows a strict hierarchy: entities own operations, interfaces expose a subset, and state machine transitions reference entity operations. An operation has exactly one owner — an entity or a domain service, never both.
Example:
operation Order.place(customer: CustomerId, lines: LineItem[]) -> Order
safe: false
idempotent: false
owned by: Order
satisfies [REQ-ORD-002]
precondition linesNotEmpty(lines)
precondition customerKnown(customer)
postcondition stateBecomesPlaced(state_before, state_after)
emits OrderPlaced
emits OrderRejected // error event
Precondition & Postcondition
A precondition and a postcondition are named predicates attached to an operation. They are part of the operation’s contract.
- A precondition is what must be true before the operation runs. On an interface operation, a precondition references only the operation’s arguments (the interface cannot see state). On an entity operation, a precondition references both state and arguments:
P(state, arguments) -> boolean. Every precondition declares a violation reason — a named error that explains what failed and that becomes the error event when the precondition is rejected. - A postcondition evaluates after the operation runs. It compares the state before and after the call, plus the arguments:
P(state_before, state_after, arguments) -> boolean.
Preconditions enable the operation; postconditions validate it. Together they describe what the operation guarantees.
Example:
operation Order.addLine(order, line: LineItem) -> Order
precondition orderIsDraft(order.state) reason OrderNotDraft
precondition lineQuantityPositive(line) reason InvalidQuantity
postcondition totalIncreasedByLineAmount(state_before, state_after, line)
The precondition orderIsDraft references entity state. The precondition lineQuantityPositive references only the argument. Each carries a reason that becomes an error event if the precondition fails.
Software System’s Interface
A software system communicates with its collaborators — other bounded contexts, frontends, end users — through typed messages. The metamodel distinguishes two directions:
Inbound communication (the System is driven). Direction: Collaborators → System. Accepted inputs: Commands, Queries, and External Events. The system handles commands and queries it receives, and it consumes events from upstream dependencies.
Outbound communication (the System is driving). Direction: System → Collaborators. Dispatched outputs: Events, Commands, and Queries. The system publishes its own events, and it invokes commands or queries on external dependencies.
See Commands, Queries & Events for the full definitions of each message type.
Message Timeline
The three message types map to three points on the timeline of a consistency boundary. This is the simplest way to remember what each one is for:
| Message | Timeline | Characteristics |
|---|---|---|
| Event | Past | Represents a fact. Append-only. Cannot be retracted, only superseded. |
| Query | Now | Represents current state. Safe and idempotent — no side effects. |
| Command | Future | Represents an intention to change state. Can succeed or fail. |
A command sent now is an intention about the future: it asks the system to transition into a new state. A query asks what the state is at this moment. An event records what already happened. If a message does not fit one of these three, it does not belong at the boundary.