Home

/

The Production-Ready Playbook

/

A Reference Service

A Reference Service

Chapter 10
Part III
4
min read

You have climbed the ladder. Every pattern with its payoff and its skip-if. Read in isolation they are a vocabulary; the question a practitioner actually asks is how they fit together in one running thing. So here is one running thing, and it is the same app you have met in every chapter so far.

A customer opens the food-delivery app, builds an order from a restaurant's menu, and taps place. From there the work fans out. The order is written and an OrderPlaced event leaves through the Outbox. A kitchen worker and a courier-assignment worker pick it up. A payment Saga charges the customer and pays out the restaurant and the courier. The customer watches live status climb from placed to delivered. Three sides touch this: the customer, the restaurant (the tenant), and the courier. One slice of the marketplace, assembled.

We will walk it in the order an order flows: in through the API, down to the database, out to the queue, into the workers, through the payment Saga, and back to the customer as live status. At each stop, the pattern doing the work is named where it lives. Nothing here is a toy fragment. It is the shape you would copy.

The Order Service

The place-order path: API, gateway, outbox

An order comes in over HTTP. The endpoint does almost nothing itself; it hands the request to a mediator and returns. That thin edge is deliberate. The Mediator (MediatR, Jimmy Bogard) keeps the controller from knowing anything about how an order is stored or queued, and the command/query split means the read side never touches the write side.

app.MapPost("/orders", async (PlaceOrder cmd, IMediator mediator, CancellationToken ct) =>
{
    var id = await mediator.Send(cmd, ct);
    return Results.Accepted($"/orders/{id}", new { id });
});

app.MapGet("/orders/{id:guid}", async (Guid id, IMediator mediator, CancellationToken ct) =>
    await mediator.Send(new GetOrderStatus(id), ct) is { } status
        ? Results.Ok(status)
        : Results.NotFound());

The handler is where the persistence altitude shows up. Two patterns share one database transaction: the OrderGateway, a Data Gateway (Fowler, Patterns of Enterprise Application Architecture) that owns the SQL, and the Outbox (Richardson, microservices.io) that records the OrderPlaced intent-to-publish in the same commit. This is the part people get wrong. If you write the order row and then publish to the queue as a separate step, a crash between them either drops the order or duplicates it. The customer is charged for food no kitchen ever sees, or the kitchen cooks an order twice. The Outbox closes that gap by making the message part of the write.

public sealed class PlaceOrderHandler(IOrderGateway gateway) : IRequestHandler<PlaceOrder, Guid>
{
    public async Task<Guid> Handle(PlaceOrder cmd, CancellationToken ct)
    {
        var order = Order.Place(cmd.RestaurantId, cmd.Lines, cmd.Total); // State: starts Placed
        await gateway.InsertWithOutbox(order, ct);                        // Data Gateway + Outbox, one tx
        return order.Id;
    }
}

The menu those Lines came from is a Composite (GoF): sections hold items, items hold modifier-groups, modifier-groups hold modifiers, and a price or an availability flag rolls up the same way at every level. The customer priced the order by walking that tree on the client; the server stores the flat lines it produced. The Composite earns its keep on the read side of the menu, not here, but it is the structure the order references.

The gateway itself is hand-written SQL behind a thin interface, calling a stored procedure through Dapper. No ORM, no change-tracking. The procedure inserts the order and the outbox row inside a single transaction, so either both land or neither does. Tenancy is not passed as a parameter you might forget; the restaurant is the tenant, and tenant_id is set as a session variable that Row-Level Security reads.

public sealed class OrderGateway(IDbConnectionFactory factory, ITenantContext tenant) : IOrderGateway
{
    public async Task InsertWithOutbox(Order order, CancellationToken ct)
    {
        using var conn = await factory.OpenAsync(ct);
        using var tx = await conn.BeginTransactionAsync(ct);
        await conn.ExecuteAsync(
            "select set_config('app.tenant_id', @value, true)",
            new { value = tenant.Id.ToString() }, tx);            // RLS sees this for the tx
        var msgId = Guid.NewGuid();
        var payload = JsonSerializer.Serialize(new OrderPlaced(order.Id, order.RestaurantId, order.Total));
        await conn.ExecuteAsync("place_order",
            new { order.Id, Tenant = order.RestaurantId, order.Total, MsgId = msgId, Payload = payload },
            tx, commandType: CommandType.StoredProcedure);        // inserts order + outbox row
        await tx.CommitAsync(ct);
    }
}

If the OrderPlaced message isn't written in the same transaction as the order, you don't have a queue. You have a race.

Multitenancy here is the dense default from the persistence chapter: one database, one schema, a tenant_id column on every restaurant-owned table, and a Row-Level Security policy (PostgreSQL RLS) that filters every query to the current tenant. The application sets app.tenant_id once per request; the database enforces isolation whether the developer remembers to filter or not. A query for one restaurant's orders can never return another's, even when the code is wrong. That is the whole point of putting the guarantee in the engine rather than the query.

the-pareto-stack-cloud-design-patterns-for-small-teams
the-ladder-of-altitudes
how-to-read-this
object-level-the-patterns-that-earn-their-keep
decorator
state
component-level-structuring-one-service
ports-and-adapters-hexagonal
mediator-the-commandquery-split
data-persistence
optimistic-concurrency
messaging-scale
outbox
resilience-staying-up-when-dependencies-dont
rate-limiting-throttling
timeout-fallback
the-composed-pipeline
observability-diagnostics-seeing-inside-production
metrics-the-four-golden-signals
externalised-configuration
hosting-cloud-agnostic-by-default
sidecar-ambassador
orchestrator-agnostic-deploy
a-reference-service
the-relay-outbox-to-queue
the-payment-saga-charge-pay-out-compensate
the-over-engineering-tax
conclusion-production-ready-deliberately
the-pattern-quick-reference-card
altitude-3-data-persistence
altitude-5-resilience
the-skip-list
full-event-sourcing-for-crud
robert-c-martin-uncle-bob-the-house-authority-for-structure
altitude-2-component
altitude-4-messaging-scale
altitude-6-observability-diagnostics

Download the full PDF for free?

Free download — no account required

Get the PDF
Get the PDF
Related Chapters
Free Download
Get the full PDF
All pages, including all code examples, diagrams, and the appendix reference card.
No spam. Unsubscribe at any time.
Your email won't be shared.
Oops! There's a problem with your request. We're working on fixing it. Please try again later.