The problem. You want to add behaviour to an object without modifying it or subclassing it for every combination. An order's price is rarely the base total. Surge multiplies it at peak hours, a promo code knocks money off, and a loyalty tier shaves a percentage, and any of the three can apply at once.
public sealed class SurgePricing(IPricing inner, ISurgeClock clock) : IPricing
{
public Money PriceFor(Order order) =>
inner.PriceFor(order) * clock.MultiplierAt(order.PlacedAt, order.Restaurant);
}
public sealed class PromoPricing(IPricing inner, PromoCode code) : IPricing
{
public Money PriceFor(Order order) => code.ApplyTo(inner.PriceFor(order));
}Each decorator implements the same IPricing it wraps, so they stack: loyalty around promo around surge around the base total, each unaware of the others.
What it buys you in production. The adjustments stay out of the core total. Base pricing computes the sum of items and nothing else; surge, promo, and loyalty live in their own wrappers you compose at registration, in an order you control. Add a "first order free delivery" rule next month as one more decorator, no edit to the four that already work. This is the in-process cousin of the Proxy and the middleware pipeline you will meet at the component altitude.
Skip-if. You only ever need one adjustment and it will never stack. At that point an extension method or a single inline check is less ceremony. Decorator pays off when behaviours combine, because the alternative is a subclass explosion for every permutation of surge, promo, and loyalty.
In the front-end. The React equivalents are hooks and higher-order components. A
withRetrywrapper around a fetch hook, or auseSurgeBannerthat layers onto a price display, adds behaviour to a component without rewriting it.
The problem. You want to turn a request into an object, so you can queue it, log it, retry it, or undo it, rather than calling a method and losing the call the instant it returns. Placing an order, cancelling one, adding an item to a cart: each is an intent worth capturing as a value, not a method call that vanishes when it returns.
public interface ICommand
{
Task Execute(CancellationToken ct);
}
public sealed record PlaceOrder(Guid CartId, Guid CustomerId) : ICommand
{
public Task Execute(CancellationToken ct) =>
OrderService.Place(CartId, CustomerId, ct);
}Once PlaceOrder is a value, you can put it on a queue, write it to a log, hand it to a worker, or hold it for a retry. The invoker and the receiver stop knowing about each other.
What it buys you in production. Command is the object-level seed of everything in the messaging altitude. A PlaceOrder you can serialise is a request you can level off a queue at dinner rush, hand to a competing consumer, or replay after a failure. It also gives you an audit trail for free, because the command is the record of what the customer asked for.
Skip-if. The work runs inline and synchronously, completes immediately, and never needs queuing, logging, or undo. Wrapping a direct method call in a command object then buys you nothing but a class. Reach for it when the request has to outlive the call stack.
In the front-end. The cart is the natural home. Model "add item", "remove item", "apply promo" as command objects and undo falls out for free: keep the executed commands on a stack and an "undo" button reverses the last one. Optimistic UI gets cleaner too, because each command knows how to roll itself back when the server rejects it.
Download the full PDF for free?
Free download — no account required