EN · · 4 min read
Design Patterns as Conversations, Not Conclusions
A practical guide to using patterns as shared engineering vocabulary without turning software into a catalogue of abstractions.
Technical GuideIntermediateEvergreen
Revised
Design patterns are often taught as named shapes: Factory, Strategy, Observer, Decorator, Repository. That makes them easy to memorize and surprisingly easy to misuse. A pattern is valuable because it compresses a recurring design conversation, not because the codebase earns points for containing one.
The question I want a pattern to answer is: which pressure in this system makes this indirection worthwhile? Without that pressure, the simplest direct implementation is usually the better design.
Begin with the force, not the diagram
Every useful pattern balances competing forces. Strategy separates a varying algorithm from a stable workflow. Decorator adds behavior while preserving an interface. Adapter protects one model from another model’s vocabulary. The pattern name comes after we observe the force.
| Pressure | Possible response | Cost introduced |
|---|---|---|
| Several policies vary independently | Strategy | More types and indirection |
| Cross-cutting behavior wraps an operation | Decorator | Call order becomes important |
| External API speaks a foreign model | Adapter | Translation code must be maintained |
| Creation has complex invariants | Factory | Construction is less visible |
| Subscribers react to domain events | Observer / events | Temporal coupling and delivery semantics |
This table is deliberately conditional. “Possible response” is not “required response.”
Strategy in a real authorization workflow
Imagine Aegis evaluates different policies behind one application use case. A switch statement may be perfectly adequate when there are two stable choices. Strategy becomes useful when policies evolve separately, require isolated tests, or are selected from configuration.
public interface IAuthorizationStrategy
{
ValueTask<Decision> EvaluateAsync(
AuthorizationContext context,
CancellationToken cancellationToken);
}
public sealed class RoleStrategy : IAuthorizationStrategy
{
public ValueTask<Decision> EvaluateAsync(
AuthorizationContext context,
CancellationToken cancellationToken)
{
var allowed = context.Subject.Roles.Contains(context.RequiredRole);
return ValueTask.FromResult(Decision.From(allowed, "role-membership"));
}
}
The interface is not the achievement. The achievement is that each policy has one reason to change and the orchestration does not know its internal rules.
Decorators for observable boundaries
Decorators work especially well around application boundaries: logging, metrics, retries, caching, and authorization. They work poorly when they hide business order or silently change meaning.
The ordering must be intentional. If metrics wrap authorization, rejected requests appear in handler latency. If authorization wraps metrics, they may disappear. Neither answer is universally correct; the design should state which question the telemetry answers.
Repository is not a synonym for database
A repository should present a collection-like contract in the language of the domain. Wrapping every ORM method in an interface often adds ceremony without creating a meaningful boundary. EF Core already implements unit-of-work and repository-like behavior. Another layer earns its place only when it protects domain intent, centralizes important query semantics, or makes the persistence boundary replaceable for a real reason.
An abstraction is useful when it hides volatility or expresses intent. It is expensive when it only hides syntax.
For a use case that needs one carefully shaped query, a query service may communicate more honestly than a generic repository with GetAll() and expression parameters.
Patterns compose — and their costs multiply
A request can pass through a mediator, pipeline behaviors, strategies, repositories, factories, domain events, and adapters. Each piece may be defensible alone while the whole becomes difficult to trace.
Before adding a pattern, I ask:
- Can I name the change or risk it isolates?
- Is that pressure present now, rather than imagined?
- Does the pattern make the main execution path easier to explain?
- Can tests exercise the boundary without duplicating implementation?
- Is removing it later straightforward?
A pattern should improve the conversation
The final test is whether the pattern helps engineers state what varies, what must remain stable, and which cost the design accepts.
Record the force and the cost together
A pattern decision is incomplete when it records only the resulting diagram. A small decision record should name the pressure that existed before the pattern, the alternatives considered, the boundary affected, and the signal that would justify removing the abstraction later.
Context: authorization policies change independently.
Force: the application use case should not know each policy's rules.
Decision: select an authorization strategy behind one contract.
Cost: more types, indirection, and configuration.
Revisit when: policy selection becomes static or the contract leaks policy data.
This language keeps the pattern connected to a problem. It also gives reviewers permission to reject an abstraction whose cost is larger than the force.
Test the seam the pattern creates
Patterns move responsibilities; tests should follow that movement. A Strategy deserves contract tests for every implementation. A Decorator deserves tests for ordering and failure propagation. A Repository deserves tests that its domain language survives the persistence adapter rather than merely wrapping a query API.
The goal is not to test the pattern by name. It is to protect the seam that now allows parts of the system to vary independently.
Prefer a vocabulary over a catalogue
Design patterns are valuable because they compress a conversation. “Decorator” can quickly suggest wrapping behavior without changing the core contract. That compression works only when the team still discusses ordering, ownership, failure, and operational cost.
A pattern has earned its place when the code is easier to change and the design is easier to explain. If it only makes the architecture look familiar, the name has replaced the reasoning it was supposed to support.
The best outcome is not “we implemented Strategy correctly.” It is “the team can discuss policy variation, test it independently, and change it without surprising unrelated behavior.” Pattern names give that conversation a handle.
Good design remains contextual. Learn the canonical forms, recognize the forces, and then use the smallest version that makes the system more legible.