Home

/

The Production-Ready Playbook

/

Component Level: Structuring One Service

Component Level: Structuring One Service

Chapter 4
Part II
5
min read

A service is the unit you deploy, page on, and rewrite when it rots. Its inside is where most of your maintenance time goes, so the patterns at this altitude buy you something concrete: the ability to change one corner without the whole thing shifting under you. Several earn the slot. The spine is Dependency Injection. The rest hang off it.

The selection rule holds here as everywhere. Each pattern is worth what it buys you in production, and each has a carrying cost worth naming out loud. Reach for the ones that keep a small team moving; skip the ones that just add ceremony you have to maintain forever.

Dependency Injection

The problem: a class that builds its own collaborators is welded to them, and you can't test it or swap them without surgery.

Dependency Injection inverts that. A class declares what it needs in its constructor and receives it; something else decides what to hand over. The "something else" is usually a container. In .NET that's Microsoft.Extensions.DependencyInjection, and you register the wiring once at startup.

services.AddScoped<IOrderGateway, OrderGateway>();
services.AddScoped<IMenuGateway, MenuGateway>();
services.AddSingleton<IPaymentAdapter, StripePaymentAdapter>();

// The consumer just asks for what it needs.
public sealed class OrderService(IOrderGateway orders, IPaymentAdapter payments)
{
    public Task<Order> Place(PlaceOrder cmd) => /* ... */;
}

What it buys you in production: every dependency becomes a seam. You swap StripePaymentAdapter for the provider a new market needs without touching OrderService. You pass a fake gateway in a unit test and place an order without ever hitting the database. Lifetimes (singleton, scoped, transient) become a deliberate decision instead of an accident, which matters the day a singleton accidentally captures a per-request tenant_id and serves one restaurant's orders to another.

DI is also where the Singleton skip from Chapter 3 lands properly. You almost never need the global-access Singleton, because a container-managed singleton lifetime gives you one instance without the static field and the testing pain that comes with it.

Inversion of Control and the DI pattern are Martin Fowler's framing, set out in "Inversion of Control Containers and the Dependency Injection pattern" (2004), and the "D" in Robert C. Martin's SOLID, the Dependency Inversion Principle. This is the one pattern in the book with no real skip-if; below a trivial script, you want it.

Every dependency you inject is a seam. Every seam is a place you can change your mind later.

Data Gateway (and stored procedures)

The problem: SQL scattered through your service couples business logic to table shapes, and the first schema change sends you hunting for query strings in twelve files.

A Data Gateway is a thin object that owns the SQL for a slice of data and exposes it behind an interface (Fowler, Patterns of Enterprise Application Architecture, 2002; Table Data Gateway). Callers see methods. The gateway sees connections, parameters, and stored procedures. Nothing leaks.

This is where the book states its data stance plainly. SQL-first: a thin Data Gateway over stored procedures, not a heavy ORM. You write the SQL, the database owns the query plan, and the gateway is fifty lines you can read in one sitting. We use Dapper for the mapping and ADO.NET underneath. We do not use Entity Framework, and the reason is the rest of this section.

This is one of three fuller anchor listings in the book, so here it is properly: a stored function, the interface a caller depends on, and the Dapper implementation behind it. This is the OrderGateway, the same one the reference service in Chapter 10 wires up.

-- Migration, checked into source control alongside the gateway.
CREATE OR REPLACE FUNCTION order_get_active_for_tenant(
    p_tenant_id uuid,
    p_as_of     timestamptz)
RETURNS TABLE (
    order_id    uuid,
    tenant_id   uuid,
    customer_id uuid,
    status      text,
    total_minor bigint,
    currency    text,
    placed_at   timestamptz)
LANGUAGE sql STABLE AS $$
    SELECT o.order_id, o.tenant_id, o.customer_id,
           o.status, o.total_minor, o.currency, o.placed_at
    FROM   "order" AS o
    WHERE  o.tenant_id = p_tenant_id
      AND  o.status NOT IN ('delivered', 'cancelled')
      AND  o.placed_at <= p_as_of
    ORDER BY o.placed_at;
$$;
public interface IOrderGateway
{
    Task<IReadOnlyList<Order>> GetActive(Guid tenantId, DateTime asOf, CancellationToken ct);
}

public sealed class OrderGateway(IDbConnectionFactory factory) : IOrderGateway
{
    public async Task<IReadOnlyList<Order>> GetActive(
        Guid tenantId, DateTime asOf, CancellationToken ct)
    {
        await using var conn = await factory.OpenAsync(ct);

        var command = new CommandDefinition(
            "SELECT * FROM order_get_active_for_tenant(@TenantId, @AsOf)",
            new { TenantId = tenantId, AsOf = asOf },
            cancellationToken: ct);

        var rows = await conn.QueryAsync<Order>(command);
        return rows.AsList();
    }
}

The function returns snake_case columns, so the gateway relies on Dapper's DefaultTypeMap.MatchNamesWithUnderscores = true, set once at startup, to bind total_minor to TotalMinor and the rest.

What it buys you in production: the gateway is the only place that knows the Order table exists. Change a column and you change one file and one procedure, both of which a reviewer can read end to end. The query plan lives in the database where a DBA can tune it without a redeploy, which matters when the live-orders board reloads every few seconds at dinner rush. The interface keeps the rest of the service ignorant of SQL, so it stays unit-testable with a fake. And there is no translation layer between what you wrote and what the database runs, so what you read in the procedure is what executes.

Data Gateway over a stored procedure

The skip-if, stated as a worked over-engineering example: reach for a heavy ORM, and Entity Framework is the canonical one. EF buys you scaffolding speed on day one and a maintenance tax forever after. Change-tracking decides on its own what to write and when, so a stray assignment to Order.Status becomes an UPDATE you didn't ask for. LINQ-to-SQL translation is a guessing game, where a query that reads fine in C# silently materialises the whole Order table into memory and filters it there, or emits a join no one would write by hand. Migration drift creeps in when the model and the database disagree, and a junior staring at a generated migration cannot reason about what it will do to a table full of live orders. None of that is wrong because EF is bad software; it's wrong because it hides the database from the small team that has to operate it. A framework you can't reason about is a tax, not a shortcut.

The gateway is fifty lines you can read in one sitting. That is the whole point.

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.