Skip to content
Slicekit

Concepts

CQRS and domain events

How commands, queries and events flow through Wolverine, and the transactional outbox that makes messaging reliable.

View .md
On this page

Commands and queries

Slicekit uses Wolverine as its mediator and message bus. A command changes state; a query reads it. Both are messages, dispatched to a handler discovered by convention.

// command
await bus.InvokeAsync(new SendInvoice(invoiceId));

// query
var invoice = await bus.InvokeAsync<InvoiceDto>(new GetInvoice(invoiceId));

Handlers are static methods that take the message plus whatever services they need as parameters. Dependencies are injected per call, so handlers stay easy to test in isolation.

Raise vs publish

The two verbs are not interchangeable, and keeping them distinct keeps the model honest:

  • Aggregates raise events. Raising records a fact inside the domain; it does not send anything.
  • Dispatchers publish events. After the change is committed, raised events are published to handlers and to the outbox.
// inside the aggregate
project.Raise(new ProjectArchived(project.Id));

// the infrastructure publishes it after SaveChanges
await publisher.PublishAsync(raisedEvent);

The transactional outbox

Publishing an event and committing the database change must not drift apart. Wolverine’s outbox stores outgoing messages in the same transaction as the state change, then relays them to RabbitMQ once the transaction commits. If the process dies mid-flight, the relay picks up where it left off: no lost messages, no double sends.

Aggregate raises

records a fact in the domain

One transaction

state + outbox row commit together

Relay forwards

resumes after a crash

RabbitMQ

integration events

Handlers react

at least once

Publishing and persistence never drift apart: if the process dies mid-flight, the relay picks up where it left off: no lost messages, and consumers are idempotent so a redelivery is harmless.

Integration events

Some events cross service boundaries. Those are published to RabbitMQ as integration events and consumed by handlers, in this app or another. The outbox guarantees at-least-once delivery: an event is never lost, and consumers are written idempotent so a rare redelivery is harmless.

Handling an event

A handler subscribes simply by accepting the event type:

public static class WhenProjectArchived
{
    public static async Task Handle(ProjectArchived e, AppDbContext db, CancellationToken ct)
    {
        // react: revoke access, send a notification, update a read model…
    }
}

Where audit fits

Audit events ride the same logging pipeline rather than a bespoke table; they are emitted through Serilog and shipped to Loki. Retention is an operations concern, not an application one. See observability.