← Back to Domain-Driven Design

Domain-Driven Design

Creating Domains

How MiniVerine finds Envelope, Messaging, and Sagas inside the larger problem of building a bus.

A domain is an area of activity and knowledge. You do not create that activity by drawing a folder. You create a model of it, and you find smaller subdomains where the work, language, and rules pull apart.

MiniVerine is a Wolverine-shaped in-process bus. Its wider domain is message handling. Inside that problem, Envelope, Messaging, and Sagas answer different questions. Those parts of the domain are implemented. Most of the application pipeline, including retries and persistence, is not.

Whose problem compiles?

Envelope metadata, wire names, and saga identity belong to the bus model. Postgres, RabbitMQ, and HTTP are ways that model may be stored or moved. They are not what an envelope is.

The project references make that dependency direction compile. MiniVerine.csproj does not reference the adapter projects. Each adapter points back to it:

<ItemGroup>
  <ProjectReference Include="..\MiniVerine\MiniVerine.csproj" />
</ItemGroup>

The adapter projects are empty today. PersistencePlan and TransportsPlan describe where their future ports and implementations belong. They are specifications, not running persistence or transport.

The bus model compiles without Npgsql, RabbitMQ, or an HTTP stack. That direction keeps adapter packages out of Envelope. It cannot stop somebody adding an unrelated string or hosting interface; the model still needs review.

Find the questions that pull apart

I treat MiniVerine’s three Domain folders as subdomains because each owns a different question:

  • Envelope — what travels as one unit of work? The body, identity, destination, timestamps, headers, attempts, and bytes.
  • Messaging — how does a CLR type get a stable wire name, and what happens when an incoming name is unknown?
  • Sagas — which process-manager instance does a message belong to, and how is a timeout represented as data?

Those are domain questions, not just directory names. The folders make the answers visible, but the ownership is the important part.

Envelope carries MessageType and SagaId, but it does not decide either one:

public record Envelope(
    EnvelopeId Id,
    Message Message,
    MessageType MessageType,
    Destination Destination,
    CorrelationId CorrelationId,
    ConversationId ConversationId,
    SagaId SagaId,
    SentAt SentAt,
    DeliverBy DeliverBy,
    Headers Headers,
    ContentType ContentType,
    Attempts Attempts,
    EnvelopeData Data);

MessageTypeNaming owns the wire-name rule. SagaIdentityNaming owns correlation to a saga instance. If either type lived under Envelope merely because Envelope carries it, the wrapper would own rules from two neighbouring problems.

That is the practical test for a subdomain: it can finish a sentence about its own rules without borrowing a neighbour’s implementation.

Classify capabilities, not packages

Subdomains are not equal. Some are unique and hard. Some are custom but simple. Some are already solved. Lazebny lays out Khononov’s three questions. Asked of MiniVerine, they fall like this.

MiniVerine’s core is the inspectable message-handling kernel. Its envelopes have named failures, message lookup can return an unknown result, and saga identity needs no I/O. That is the reason to own this bus rather than adopt another one.

Host wiring is supporting. UseMiniVerine() and MiniVerineOptions are custom because the kernel must run inside a process. They do not contain the rules that make the kernel distinctive.

Durable storage and broker transport are generic capabilities here. Postgres and RabbitMQ are technologies chosen to implement them, not subdomains by themselves. Rebuilding a database or broker would not make MiniVerine more itself.

The category depends which problem you are standing in. Broker transport is generic for MiniVerine and core for the people building RabbitMQ. MiniVerine’s kernel is core to this product. To a product that takes the bus as a library, it is generic: that product could swap Wolverine.

Folders are evidence, not the boundary

The separate adapter projects stop the core from taking an adapter dependency. Inside MiniVerine.csproj, Domain and Hosting still share a project, and that project references Microsoft.Extensions.Hosting. A folder cannot stop somebody adding IHostedService to Envelope.

The names, dependency direction, tests, and review rules work together. None is the subdomain. They are ways to keep the model from sliding after the subdomain has been found.

A named subdomain still needs a place where its words mean one thing. Bounded Contexts is that solution boundary.