Home

/

The Production-Ready Playbook

/

Object Level: The Patterns That Earn Their Keep

Object Level: The Patterns That Earn Their Keep

Chapter 3
Part II
5
min read

This is the bottom rung. Before you organise a service, before you choose a database, you organise the code inside a single class or a small cluster of them. That is what the object-level patterns do: they give shape to behaviour that would otherwise sprawl into branching and copy-paste.

The Gang of Four catalogue (Gamma, Helm, Johnson, Vlissides, Design Patterns, 1994) is the canonical source here, and it is also where over-reach starts. Twenty-three patterns is a curriculum, not a toolkit. The patterns below are the ones a small team reaches for again and again. Most come from the Gang of Four; two, Money and the Null Object, come from outside it, because the object altitude was never only theirs. Each gets the four beats we hold for every pattern in Part II: the problem in a sentence, the shape, what it buys you in production, and the skip-if.

We anchor them all to one running example, carried through the whole book: a food-delivery marketplace with three sides, the customer who orders, the restaurant that cooks (the tenant), and the courier who delivers. The Order you meet here as a State machine is the same Order you will see survive a flaky payment gateway in the resilience chapter and get assembled end to end in the reference service. Watch the patterns compose around it.

Patterns are vocabulary, not architecture. Knowing the word "Strategy" does not design anything. It just means two engineers can name the same thing.

What the compiler already gave you

Some GoF patterns won so completely that the language absorbed them. You will not find them taught here, because teaching them would mean teaching you to hand-roll something the runtime hands you for free.

Observer is the clearest case. In 1994 you wrote a subject that held a list of observers and looped over them on change. In C# that is an event, or IObservable<T> if you want the reactive shape. Iterator is IEnumerable<T> and yield return; you have never written a MoveNext() by hand and you never should. Builder, for most uses, collapsed into object initialisers, collection initialisers, and with expressions on records.

When the compiler gives you the pattern, the pattern is not a design decision any more. It is syntax. Reaching for the textbook version signals you learned the catalogue but not the language.

Strategy

The problem. You have one operation with several interchangeable algorithms, and the choice belongs to the caller or the configuration, not to a branch buried in the method. When an order is ready, you have to pick a courier, and "pick" means different things in different cities: the nearest courier, the one who will get there fastest given traffic, or the cheapest to dispatch.

public interface ICourierMatchingStrategy
{
    Courier? Match(Order order, IReadOnlyList<Courier> available);
}

public sealed class NearestCourier : ICourierMatchingStrategy
{
    public Courier? Match(Order order, IReadOnlyList<Courier> available) =>
        available.MinBy(c => c.DistanceTo(order.Restaurant));
}

public sealed class FastestCourier : ICourierMatchingStrategy
{
    public Courier? Match(Order order, IReadOnlyList<Courier> available) =>
        available.MinBy(c => c.EtaTo(order.Restaurant));
}

The dispatcher holds an ICourierMatchingStrategy and never knows which one it got. New matching rule (cheapest, highest-rated, batched), new class, no surgery on the old ones.

What it buys you in production. Strategy is the antidote to the growing switch. Each algorithm is isolated, unit-testable on its own, and addable without touching the others. That last property is the Open/Closed Principle, the "O" in Robert C. Martin's SOLID (Agile Software Development: Principles, Patterns, and Practices, 2002): open to extension, closed to modification. It also makes behaviour configurable: the same binary runs nearest-courier matching in a dense city and fastest-courier in a sprawling one, decided per tenant at startup.

Skip-if. You have one algorithm, or two that will never grow past two. A single if is not a design smell; it is an if. Strategy earns its keep when the set of behaviours is open-ended or selected at runtime, not when you are dressing up a boolean.

In the front-end. The customer's restaurant list needs the same shape. Sort by delivery time, rating, or price, and filter by cuisine or dietary tag, each a small strategy the React component selects from a dropdown rather than a nest of conditionals in the render path.

Adapter

The problem. You depend on an interface your code expects, but the thing you actually have speaks a different one, often because it is a third-party SDK or a legacy class you cannot change. Charging a customer means calling a payment provider, and Stripe, Adyen, and a local processor each expose their own shape.

public interface IPaymentProvider
{
    Task<PaymentResult> Charge(Money amount, Card card, CancellationToken ct);
}

// The vendor SDK exposes its own shape. Wrap it.
public sealed class StripePaymentProvider(StripeClient client) : IPaymentProvider
{
    public async Task<PaymentResult> Charge(Money amount, Card card, CancellationToken ct)
    {
        var intent = await client.PaymentIntents.CreateAsync(
            new PaymentIntentCreateOptions
            {
                Amount = amount.Cents, Currency = amount.Currency, PaymentMethod = card.Token
            }, cancellationToken: ct);
        return new PaymentResult(intent.Status == "succeeded", intent.Id);
    }
}

Your order code talks to IPaymentProvider. The vendor's surface lives behind the adapter and nowhere else.

What it buys you in production. The vendor's shape stops at the seam. When you swap Stripe for Adyen, or add a regional processor a new market demands, you write one new adapter; the rest of the order flow never learns the supplier changed. It also makes the dependency mockable, because your code depends on your interface, not the SDK's. One altitude up this same instinct grows into the Anti-Corruption Layer, which insulates not one call but a whole model.

Skip-if. The external interface is already clean and you only call it in one place. An adapter you write once and never benefit from is a layer of indirection charged against the reader's attention. Wrap things you swap, mock, or want to quarantine.

In the front-end. The checkout screen wraps the maps SDK and the payment SDK the same way. A thin IMapView or IPaymentSheet around Google Maps or Stripe Elements keeps your components from binding to a vendor's prop names you do not control.

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.