# Chapter 7 — Domain events + the outbox

> **Goal:** react to "an order was placed" reliably — the reaction runs even if the process crashes right
> after the sale.
> **You'll write:** an event, a domain method that raises it, a component that places orders, and a
> durable handler.

In Chapter 4 we enqueued a job explicitly. Sometimes you'd rather have the *domain* announce that something
happened and let any number of handlers react — without the code that placed the order knowing who's
listening. That's a **domain event**. And if the reaction must not be lost (you really will refund that
card), it should be delivered through a **transactional outbox**: the event is written **in the same
database transaction** as the order, so it can never be committed without the event, and a background
processor delivers it after commit — retrying until it succeeds.

## 1. Placing an order is a domain operation

Chapter 3's order form saves an `OrderModel` with `Order.Create(model)`. That is exactly right for data
entry (a member of staff types an order in) and exactly wrong for announcing anything, because a form save
writes properties and nothing in it says that what just happened was a *sale*. It records a row; it isn't the
business event, so it isn't where the event belongs.

A customer buying a product is a different thing, and it gets its own path: a static factory on the aggregate
that raises the event, and a small component that inserts what it built. Four small additions.

**The event.** `Features/Orders/OrderEvents.cs` — one record per thing that happened:

```csharp
namespace Shop.Features.Orders;

[LocalOnly]
public sealed record OrderPlaced(Guid Id) : IEvent;
```

`[LocalOnly]` keeps it on the server. Without it, a signed-in browser could publish an `OrderPlaced` for an order
nobody placed, and its handler would send a receipt for it. Only `Order.Place` should ever say a sale happened.

**Raising it.** Announce the change from the same code that makes it, so an order can never be placed
without saying so. Here is chapter 3's `Features/Orders/Order.cs` with `Place` added — the whole file, so you
can see where it goes:

```csharp
using System.ComponentModel.DataAnnotations;

namespace Shop.Features.Orders;

public sealed class Order : Aggregate<Guid, Timestamps, Versioned, Model, Deletable>
{
    [Range(0, 1_000_000)]
    public decimal Total { get; private set; }

    public Guid ProductId { get; private set; }

    public DateTime Placed { get; private set; }

    public static Order Place(Guid productId, decimal total, DateTime now)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(total);

        var order = new Order { Id = Guid.CreateVersion7(), ProductId = productId, Total = total, Placed = now };
        order.Raise(new OrderPlaced(order.Id));
        return order;
    }
}
```

The fields are chapter 3's, untouched, so the staff pages keep working. `Place` is the aggregate's **factory**:
the domain's way to create an order, which is why `Order` still declares no constructor. `Raise` comes from
`Aggregate<Guid>`, and the event sits on the aggregate until it is saved, which is what makes the next part
atomic.

**Placing it.** `Features/Orders/PlaceOrder.cs` — a "Buy" button that places an order for one product:

```csharp
namespace Shop.Features.Orders;

// A "Buy" button. A sale is a domain operation, not a form edit, so it goes through Order.Place — the
// factory that announces it — and Order.Create inserts the order Place built.
public sealed partial class PlaceOrder : Component
{
    private bool _placing;

    public Guid ProductId { get; set; }

    public decimal Price { get; set; }

    private async Task Place()
    {
        _placing = true;
        try
        {
            var order = Order.Place(ProductId, Price, DateTime.UtcNow);
            await Order.Create(order);        // the order AND its OrderPlaced, one transaction
        }
        finally
        {
            _placing = false;
        }
    }

    protected override Component? Render() =>
        Ui.Button.Primary.Sm.Disabled(_placing).OnClick(Place)["Buy"];
}
```

`Order.Create(order)` is the create for an aggregate you built yourself. It saves through the same
interceptors a form save does, so the order and the event it carries are written in one transaction. The
`_placing` flag disables the button while the save runs, so a double click can't buy twice.

Drop it into `ProductsPage`'s actions column (with `using Shop.Features.Orders;` at the top of that file):

```csharp
PlaceOrder.ProductId(p.Id).Price(p.Price)
```

**Receipts follow sales now.** Take chapter 4's enqueue back out of `CreateOrder` — the handler in
section 3 queues the receipt from the event instead, durably.

## 2. Nothing to remember

There is nothing to register. The outbox is on whenever the database is, its table is mapped by
`RaskAppDbContext`, and it was created by the first migration, so there is nothing to migrate either.

Every `SaveChanges` runs the outbox's interceptor beside the timestamps and the version bump. That's why
`PlaceOrder`'s `Order.Create` gets all three, exactly as chapter 2's form saves do.

## 3. React to the event

An event can have any number of handlers, and **each handler chooses** how it is run:

- `IEventHandler<OrderPlaced>` runs in memory straight after the commit. It's fast, but it's gone if the process
  dies at that moment. That suits a dashboard counter.
- `IDurableHandler<OrderPlaced>` is written to the outbox in the order's own transaction, and runs from there after
  the commit, retried until it succeeds. That suits a receipt.

A receipt must not be lost, so `Features/Orders/OrderPlacedHandler.cs` is durable. It logs the sale and queues
chapter 4's receipt job, so the receipt is now derived from the order's own transaction:

```csharp
using Microsoft.Extensions.Logging;
using Shop.Features.Shared;

namespace Shop.Features.Orders;

public sealed class OrderPlacedHandler(ILogger<OrderPlacedHandler> logger)
    : IDurableHandler<OrderPlaced>
{
    public async Task Handle(OrderPlaced e)
    {
        logger.LogInformation("Order {Id} placed", e.Id);
        await Jobs.Enqueue(new SendOrderReceipt(e.Id));
    }
}
```

It's the one thing in this chapter you implement rather than call. Nothing registers it: it's found at build time.

Because its outbox row committed atomically with the order, the handler is guaranteed to run **eventually**,
even if the app is killed the instant after the sale — which closes the gap Chapter 4 left open. Delivery is
**at-least-once**, so make the handler safe to repeat: it can run twice if the process dies between the work
and the acknowledgement. Here that could mean two receipt jobs, so a real shop has the job check whether a
receipt already went out before sending another.

> **Outbox or job?** Both end in a background worker, and the distinction is worth holding onto: the outbox
> delivers what is *derived from* a transaction (the order committed, so confirm it), and
> [jobs](https://rask.sh/docs/guides/04-background-jobs/) run what you *schedule* (in an hour, purge stale carts). A confirmation email
> belongs to the order's transaction. A nightly cleanup does not.

> **What raises nothing.** Editing an order through `UpdateOrder` announces nothing: a form save writes the
> model's properties and calls no method, so nothing raises an event. When a change *is* something the business
> cares about (a cancellation, a shipment) give `Order` a method for it that raises its event, and call it
> through `Order.Update(id, o => o.Cancel(now))`.

## Verify

- Clicking **Buy** on `/products` writes an `OutboxMessage` row in the same transaction as the order.
- The `OrderPlacedHandler` log line appears within the poll interval, and the row's `ProcessedAt` is set
  while `Error` stays null and `Attempts` stays `0` — that combination is what "delivered cleanly" means. A
  message that can't be deserialized doesn't throw; it records an error and retries until `MaxAttempts`.
- A receipt job row follows, and then the receipt in `mail-pickup` — the chain from Chapters 4 and 5, now
  starting from the transaction.
- Kill the app immediately after buying, restart it — the handler still runs.
- **Go further:** watch the outbox, job and mail counters move, in that order, on `/ops`
  ([Chapter 10](https://rask.sh/docs/guides/10-ops/)).

**Learn more:** [outbox](https://rask.sh/docs/guides/outbox/) · [Rask.Data](https://rask.sh/docs/guides/data/) · [background jobs](https://rask.sh/docs/guides/jobs/)

Next → **[Chapter 8: Production SQLite](https://rask.sh/docs/guides/08-production-sqlite/)**
