# Domain and integration events

> Publish and handle domain and integration events over Wolverine, with the transactional outbox for reliability.

This is the how-to companion to [CQRS and domain events](/docs/cqrs-and-events), which covers the
concepts. Here you wire the real thing: define an event, raise it from an aggregate, handle it, and
route an integration event out to other services through the transactional outbox.

## Two event types

HrKennisbank distinguishes two kinds of event, each with its own marker, location, and transport.

| Type              | Marker              | Where it lives                       | Scope         | Transport                              |
| ----------------- | ------------------- | ------------------------------------ | ------------- | -------------------------------------- |
| Domain event      | `IDomainEvent`      | `src/HrKennisbank.Core/Domain/Events/`     | In-process    | Wolverine local queue                  |
| Integration event | `IIntegrationEvent` | `src/HrKennisbank.Core/IntegrationEvents/` | Cross-context | RabbitMQ exchange `hrkennisbank.integration` |

Both markers are empty interfaces in `src/HrKennisbank.Core/Domain/Primitives/`. The split is enforced by
`EventTaxonomyTests`: an `IDomainEvent` must live under `Domain.Events`, an `IIntegrationEvent` under
`IntegrationEvents`, and implementing both is rejected.

Keep the verbs straight: aggregates **raise** domain events to record a fact; dispatchers and handlers
**publish** events to send them somewhere. They are not interchangeable.

## 1. Define a domain event

A domain event is a pure record marked with `IDomainEvent`. No EF Core, no ASP.NET, no Wolverine
types: anyone in any layer must be able to deserialize and react. Add it to a file under
`src/HrKennisbank.Core/Domain/Events/` (the existing files group by aggregate, for example `UserEvents.cs`):

```csharp
namespace HrKennisbank.Core.Domain.Events;

public sealed record ProjectCreatedEvent(Guid ProjectId, string Name) : IDomainEvent;
```

The `Domain_Events_Must_Not_Depend_On_Infrastructure` test keeps these records clean, so resist the
urge to reach for a `DateTimeOffset.UtcNow` default or an injected service inside the record.

## 2. Raise it from the aggregate

The aggregate owns its invariants and records what happened by **raising** the event. `AggregateRoot`
exposes a protected `Raise` that appends to an internal `Events` list; it sends nothing yet.

```csharp
public sealed class Project : AggregateRoot
{
    public static Project Create(Guid ownerId, string name)
    {
        var project = new Project { Id = Guid.CreateVersion7(), OwnerId = ownerId, Name = name };
        project.Raise(new ProjectCreatedEvent(project.Id, name));
        return project;
    }
}
```

`User.Create` in `src/HrKennisbank.Core/Domain/User.cs` is the canonical example: it raises
`UserCreatedEvent` the same way.

### How raised events become published

The wiring lives in `src/HrKennisbank.Core/Configuration/Messaging.cs`:

```csharp
opts.UseEntityFrameworkCoreTransactions();
opts.Policies.AutoApplyTransactions();
opts.PublishDomainEventsFromEntityFrameworkCore(x => x.Events);
```

After your slice handler calls `db.SaveChangesAsync`, Wolverine's middleware scrapes each aggregate's
`Events` collection and publishes them in-process over a durable local queue. Two consequences:

- **Handlers run only if the transaction commits.** Raised events live and die with `SaveChanges`. A
  rollback drops them.
- **`AutoApplyTransactions()` is load-bearing.** Without it the scraper is registered but never
  engages, and raised events are silently discarded. If a handler that looks correct never fires,
  check this line first.

## 3. Handle the event

Handlers are discovered by convention: a `*Handler` class with a `Handle` method whose first parameter
is the event type. No interface, no registration. Place the handler inside the slice that owns the
reaction, alongside its command and handler.

```csharp
using HrKennisbank.Core.Domain.Events;
using Wolverine;

namespace HrKennisbank.Core.Features.Projects.Welcome;

public sealed class NotifyProjectCreatedHandler(IMessageBus bus)
{
    public ValueTask Handle(ProjectCreatedEvent evt) =>
        bus.PublishAsync(new SendTemplateEmailCommand
        {
            Recipient = /* ... */,
            Subject = "Your project is ready",
            Template = EmailTemplate.Welcome,
            Model = /* ... */
        });
}
```

Many handlers can react to one event; they fan out independently. Real examples both react to
`UserCreatedEvent`: `Features/Auth/Welcome/SendWelcomeEmailHandler.cs` sends a welcome email, and
`Auditing/SecurityAuditEventHandler.cs` writes an audit row. A handler may take a `CancellationToken`
and inject services as extra parameters, exactly like a command handler.

## 4. Route an integration event through the outbox

An integration event is the public, stable contract that other services consume. It is published to
the `hrkennisbank.integration` RabbitMQ exchange. The template ships with none: `IntegrationEvents/` is empty
until your application defines its first cross-context contract.

Define it under `src/HrKennisbank.Core/IntegrationEvents/`, using only primitives, strings, and enums:

```csharp
namespace HrKennisbank.Core.IntegrationEvents;

public sealed record OrderPlacedIntegrationEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal Total,
    DateTimeOffset PlacedAtUtc) : IIntegrationEvent;
```

`Integration_Events_Must_Not_Depend_On_Internal_Types` blocks these from referencing `Domain.Events`,
`Identity`, `Features`, EF Core, or Wolverine. The contract must stay decoupled from your internals.

**Do not publish an integration event from an aggregate.** The aggregate raises a domain event; a
separate translator handler turns that into the public contract:

```csharp
// Features/Orders/PublishOrderPlacedIntegrationEvent/Handler.cs
public sealed class PublishOrderPlacedIntegrationEventHandler(IMessageBus bus)
{
    public Task Handle(OrderPlacedEvent evt, CancellationToken ct) =>
        bus.PublishAsync(new OrderPlacedIntegrationEvent(
            evt.OrderId, evt.CustomerId, evt.Total, DateTimeOffset.UtcNow));
}
```

The routing is already configured in `Messaging.cs`:

```csharp
opts.Publish(x => x.MessagesImplementing().ToRabbitExchange("hrkennisbank.integration"));
```

Outside the testing environment Wolverine persists outgoing messages with Postgres
(`PersistMessagesWithPostgresql`) and uses durable local queues. That is the transactional outbox: the
integration event is written in the same transaction as your state change and relayed to RabbitMQ once
the transaction commits. If the process dies mid-flight, the relay resumes, so delivery is
at-least-once. Write consumers to be idempotent.

## Events are not commands

A domain event records a fact and fans out to any number of handlers. A Wolverine command
(`SendTemplateEmailCommand`, `CleanupExpiredTokensCommand`) targets exactly one handler and triggers a
specific action; it implements neither marker. Reach for a command when the producer has a single
receiver in mind, when the work must run after the transaction commits without influencing its outcome,
or when you want Wolverine's retry semantics on a step that might fail. Commands ride the same outbox.

## Verify

```sh
dotnet test api/tests/HrKennisbank.Architecture.Tests --nologo
```

- `EventTaxonomyTests` and `LayerDependencyTests` pass.
- A new event in the wrong namespace fails `EventTaxonomyTests`.
- An integration event leaking a domain type fails `Integration_Events_Must_Not_Depend_On_Internal_Types`.

## Checklist

- [ ] Domain event is a pure record marked `IDomainEvent`, under `Domain/Events/`.
- [ ] The aggregate **raises** the event; nothing publishes from inside the aggregate.
- [ ] A `*Handler` with a `Handle(TEvent, ...)` method lives in the reacting slice.
- [ ] Cross-context contracts are `IIntegrationEvent` records under `IntegrationEvents/`, primitives only.
- [ ] A translator handler maps the domain event to the integration event; the outbox carries it to RabbitMQ.
- [ ] `dotnet test api/tests/HrKennisbank.Architecture.Tests --nologo` is green.
