What is a specification?
A specification is a design pattern popularized by Domain-Driven Design (DDD). Today, however, it lives well beyond DDD — variants show up in Spring Data, NHibernate, Entity Framework Core, QueryDSL, and the Ardalis.Specification library. The shared idea is one: wrap a single business rule inside a dedicated object that can judge whether a given candidate — a customer, an order, an invoice — meets a specific condition.
In its classic form, such an object exposes an isSatisfiedBy(candidate) method returning true or false, though modern implementations often represent a specification differently (more on that below). What matters is that the rule stops being a fragment of an if statement and becomes a named, standalone entity that you can pass as an argument, store in a collection, and test in isolation.
It is worth stating up front what a specification is not. It is not a rule engine, nor a form-validation framework, nor a database query. It is a small, tactical object-oriented pattern — a building block that organizes decision logic in the domain layer. As the English Wikipedia puts it, the whole idea comes down to recombining business rules by chaining them together with Boolean logic.
What problem does it solve?
In typical code, a business rule exists as a bare technical condition. "Premium customer" is somewhere balance > 100, "overdue invoice" is days > 30, and "suspicious transaction" is a tangled if over several fields. The same condition gets duplicated across a controller, a service, a database query, and a test — and its business meaning is hidden behind a number you must first decode.
The specification answers this in one move: it forces you to describe a business fact, not a technical condition. Instead of price > 100 you get PremiumCustomerSpecification; instead of days > 30 — InvoiceOverdueSpecification. This is not cosmetics. The code starts naming things the way the business names them, and the rule's definition lands in one authoritative place. That is the essence of DDD — the domain model talks about concepts, not numeric thresholds.
Who is behind it?
The pattern was popularized by Eric Evans in his book Domain-Driven Design (2003) and later developed jointly with Martin Fowler in a write-up dedicated to specifications. These are two of the most important names in object-oriented design: Evans authored the DDD approach, Fowler is one of the most frequently cited authors on patterns and refactoring. Practical treatments came later from others, including Vladimir Khorikov of Enterprise Craftsmanship and the DevIQ knowledge base.
How does it work?
The mechanism is simple. You define a shared contract (for example an ISpecification interface) with a method that evaluates a candidate, and each concrete rule is a separate class implementing that contract. The code making the decision knows nothing about the rule's details — it merely asks: "does this object satisfy you?".
In practice the API varies. In Spring Data, the Specification<Customer> type has no isSatisfiedBy() method but a toPredicate(...) that builds a fragment of a Criteria API query. Ardalis.Specification, in turn, exposes neither method, describing criteria declaratively. Despite the differences, the idea stays the same: a named rule wrapped in an object.
The real convenience appears when combining rules. Specifications are composed with AND (both conditions), OR (either one), and NOT (negation). From "invoice overdue", "notices sent", and the negation "already in collections" you build one rule: overdue AND notices sent AND NOT in collections — assembled from ready-made blocks, without writing a new, tangled condition.
Composition itself, however, is no longer an exclusive trait of specifications: in many languages a plain predicate (e.g. Predicate<Customer>) can also be composed via and, or, and negate. A specification goes a step further — it gives the rule a name, a place in the domain model, and the ability to be reused. That, rather than the logical operators, is its real advantage.
What are its key components?
The classic implementation rests on three layers:
- Specification interface — the contract with a method that evaluates a candidate.
- Abstract composite specification with
And,Or, andNotmethods. All concrete rules inherit from it, so each can immediately be combined with others. - Concrete specifications — individual rules, each expressing one clearly named condition.
On top come operator classes (AndSpecification, OrSpecification, NotSpecification) that hold the combined rules and invoke them in turn — a classic Composite pattern.
But this is only one possible form, not the one true way. A specification can equally well be a record, a sealed interface, a lambda, a function, or an enum value. What matters is the contract — evaluating a candidate and being composable — not a particular skeleton of classes.
What can it be used for?
Evans and Fowler identified three uses that still shape how we think about the pattern:
- Validation — checking whether an existing object meets a criterion (e.g. whether an order qualifies for free shipping).
- Selection — picking from a collection or database the objects that match the rule.
- Building to order — constructing a new object so that it satisfies a given specification from the start.
As Khorikov notes, the first two dominate in practice, and the biggest payoff is usually the elimination of duplicated domain knowledge. Without the pattern, the definition of, say, a "kids movie" may live in two places: once as an in-memory validation and once as a condition in a database query. A specification gathers that knowledge into a single source of truth.
How does it differ from other approaches?
A specification is clearest when set against what it replaces:
| Approach | What it is | Key difference from a specification |
|---|---|---|
| Plain if condition | a condition at the point of use | hard to reuse and to test in isolation |
| Predicate / lambda | a function returning true or false | also composes, but does not gather domain knowledge in one named place |
| Strategy pattern | choosing how to perform an action | answers "how", not "yes or no" |
| Rule engine | a large system with its own language | far heavier, lives outside the application code |
Key limitations and challenges
The biggest risk is over-engineering. For a simple application with a handful of conditions, a separate class per rule is needless overhead and file proliferation. The pattern pays off where rules are numerous, frequently combined, and repeated across the system.
The second, more serious challenge is translating a specification into a database query. A rule that works in memory on a single object cannot always be turned cleanly into a SQL condition on the database side. Loading every record into memory and only then filtering can be disastrous for performance. That is why mature implementations (for example in .NET using expression trees) build the specification so that it can be translated into a query — which complicates the code. Khorikov also warns against returning a "raw" IQueryable from a repository, citing violations of the DRY principle and the Liskov substitution principle.
When not to use a specification?
The pattern is not the default choice for every rule. It is better skipped when:
- the rule is used only once, in a single place,
- a readable, simple
ifis enough, - there is no need for reuse or a negated variant,
- the rules never need to be combined,
- the project is a plain CRUD app with no distinct domain model.
This is an important safety valve: it is easy to get carried away and write 300 specification classes where a single condition would do. A specification only pays off with genuine repetition and composition of rules.
Why does it matter?
The Specification pattern touches one of the hardest problems in software maintenance: the scattering of knowledge about business rules. In a typical system the same condition — "active premium customer", "order eligible for return", "suspicious transaction" — appears in a controller, a service, a database query, and a test. When the rule changes, you must find and fix it in every place, and missing even one is a ready-made production bug.
The specification gives a concrete answer: the rule becomes one named object — a single source of truth — and moves business logic out of the technical corners of the code into a clearly outlined domain layer. The code starts to speak the language of the business: instead of a tangled condition you see newSpecification.And(other), which reads almost like a sentence. This is not a pattern that "changes everything" — it is a precise tool for a specific problem, whose value is easiest to appreciate once it is missing from a growing project.
isSatisfiedBy.Ultimately, a specification will not fix an architecture on its own. But in the hands of a team that deliberately manages its business rules, it turns the chaos of scattered conditions into an orderly, testable, readable set of building blocks — provided you reach for it where it is genuinely needed.
