A Domain Concept is the fundamental building block of a domain model. It represents a meaningful, named thing that domain experts recognize and reason about — not a database table or a service class, but a real-world idea the business depends on. A Reservation, a Ride, an Invoice — these are domain concepts. They exist independently of any implementation choice.
What makes a concept rich enough to model is that it has not one but four distinct facets: it has a shape, it does things, it obeys rules, and it relates to other concepts. The Specy metamodel gives each of these facets a dedicated modeling mechanism. Keeping them separate — rather than collapsing them all into one definition — is what makes a model legible and changeable.
Structure — what a concept is made of
The structural view answers: does this concept have identity, or is it defined entirely by its data?
If two instances are distinguishable even when all their attributes are identical — a User account with the same name as another is still a different account — the concept is an Entity. It has a stable identifier that persists across state changes. Entities form Aggregates when a cluster of them must be mutated as a single consistency unit; the aggregate root controls all access and enforces integrity across the cluster.
If two instances with identical attributes are completely interchangeable — a Money(100, EUR) is the same value regardless of which code created it — the concept is a Value Type. Value types are immutable and compose freely into entities.
The structural view is captured with entity, aggregate, and value declarations in the domain model.
Behavior — what a concept does
The behavioral view answers: what can happen to this concept, and what does it signal when it does?
Every meaningful state change is an Operation — a named action owned by the entity, safe or unsafe, idempotent or not, with preconditions, postconditions, and zero or more emitted events. Operations are the only entry point for mutation; nothing changes a concept’s state except an explicit operation.
Operations emit Events (recorded facts), accept Commands (declared intentions), and answer Queries (safe reads). When an entity’s lifecycle is non-trivial — when it moves through discrete phases like draft → confirmed → completed → cancelled — a State Machine makes those phases and their transitions explicit, attaching guards and postconditions to each transition.
When something happens in another aggregate that this concept needs to act on, the coordination is expressed as a Reaction: it listens to an internal, error, or temporal event, evaluates a guard, and issues a command. Reactions are how aggregates collaborate without sharing transactions.
The behavioral view is captured with operation, command, query, event, state-machine, and reaction declarations.
Properties — what a concept must always satisfy
The properties view answers: what verifiable claims hold about this concept — about its state, its operations’ contracts, its lifecycle, and its agreements with other concepts? The properties layer collects everything a property-based test can target.
An Invariant is a synchronous predicate within a single aggregate: a boolean that must hold at every observable point. It is evaluated immediately on every mutation and enforced by rejection, compensation, or alert. Invariants express the rules the aggregate root is responsible for.
An Agreement formalizes a cross-aggregate truth that no single transaction can guarantee — bilateral or multilateral — and acknowledges an explicit inconsistency window. Each agreement is maintained by exactly one Reconciliation, which detects violations and issues compensating commands. When reconciliation itself fails, an Escalation Chain drives the response through retries, alternative compensations, alerts, suspends, and ultimately manual intervention.
The contracts on this concept’s operations — its Preconditions (what must be true before an operation runs) and Postconditions (what must be true after) — are also properties of this view. They are declared on the operation but verified the same way: a generator produces inputs, the operation runs, the postcondition is checked.
When the concept has a non-trivial lifecycle, its state machine carries an additional bundle of properties: every transition guard, every state-scoped invariant, every reachability claim. A property-based test can drive the entity through random sequences of valid transitions and assert these claims at every step.
The properties view is captured with invariant and agreement declarations, the precondition/postcondition clauses on operations, the state-machine constraints on entities, and the reconciliation and escalationChain declarations that maintain agreements at runtime.
Relations — how a concept connects to others
The relational view answers: how does this concept depend on, compose with, or coordinate across other concepts?
Within a bounded context, Relationships between entities express typed references — ownership, membership, association. Within a context, Modules group related concepts behind one or more controlled Interfaces, exposing only the operations the rest of the system is allowed to call. Modules are the preferred decomposition mechanism when the language stays consistent; they decompose without breaking the ubiquitous language.
When behavior spans multiple entities and ownership by any single one would be arbitrary, a Domain Service holds it — a stateless operation collection living in the same context. When external systems must be reached — a payment gateway, a search index, an upstream context’s API — an Infrastructure Service fronts that dependency through an interface, with an adapter implementing the contract. The Anti-Corruption Layer pattern is implemented as an infrastructure service. Application Services orchestrate use cases across the domain and delegate to it, holding use-case state but no domain logic. The dependency direction is strict: domain calls infrastructure (never the reverse), and application calls domain (never the reverse).
Across bounded contexts, Context Map Patterns govern the relationship: who owns the contract, who translates, who absorbs upstream change. Patterns like Anti-Corruption Layer, Customer/Supplier, or Open Host Service are not just labels — they determine which team carries which coordination burden and shape how events and commands cross the boundary.
The relational view is captured with relationship declarations inside entities, module and interface declarations for internal exposition, domain-service / application-service / infrastructure-service declarations for cross-entity behavior and external dependencies, and context map patterns for cross-context integration.
Putting it together
Consider a Reservation concept in a resource-booking domain.
-
Structure:
Reservationis an entity — two reservations for the same resource at the same time are distinct even if their attributes look similar. It owns an identifier, astatusfield, a reference to the reserved resource, and a reference to the reserving user. -
Behavior: It has operations —
confirm,cancel,expire. The lifecycle has four states:pending → confirmed → used → cancelled. Acanceloperation is only valid frompendingorconfirmed; it emits aReservationCancelledevent. A temporal event triggersexpirewhen the reservation window closes with nouse. A reaction listens forReservationCancelledand issues aNotifyUsercommand — coordination with the notification aggregate without sharing a transaction. -
Properties: The
canceloperation has a precondition (reservation.status in {pending, confirmed}) and a postcondition (reservation.status == cancelled). The state machine guarantees no transition fires from a final state. An invariant ensures a resource cannot be double-booked within the same time window. An agreement with theResourceaggregate ensures the resource’s availability count stays consistent with the number of active reservations, with a small inconsistency window covered by a reconciliation that re-checks counts on every release. -
Relations:
Reservationhas a relationship toResource(the thing being reserved) and toUser(the reserver). Both live in the same bounded context and are accessed through theReservationModule’s interface. IfResourcewere owned by a separate upstream context, the relationship would be replaced by a context map pattern — most likely an Anti-Corruption Layer that translates the upstream resource schema into the local ubiquitous language.
No single one of these four views tells the full story. Together they make the concept explicit enough to build from, test against, and trace back to the requirements it satisfies.