Robots Atlas>ROBOTS ATLAS
Architectural Pattern

What is the Specification pattern and how does it work?

What is the Specification pattern and how does it work?

The Specification pattern lets you capture a business rule as a separate, testable object that answers one question: does this object meet the condition? It is worth knowing because it tidies up logic scattered across a codebase.

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 > 30InvoiceOverdueSpecification. 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?".

Java
interface Specification<T> {                 // shared contract
    boolean isSatisfiedBy(T candidate);
}

class InvoiceOverdue implements Specification<Invoice> {
    public boolean isSatisfiedBy(Invoice i) {  // concrete rule
        return i.getDaysLate() > 30;
    }
}

// usage: the deciding code only asks whether the rule holds
Specification<Invoice> overdue = new InvoiceOverdue();
if (overdue.isSatisfiedBy(invoice)) {
    sendReminder(invoice);
}

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.

Java
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.*;

// Spring Data: no isSatisfiedBy(), a toPredicate(...) building a query fragment
class PremiumCustomerSpec implements Specification<Customer> {
    public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> q, CriteriaBuilder cb) {
        return cb.greaterThan(root.get("balance"), 100);
    }
}

// usage: the repository translates the specification into a SQL query
List<Customer> premium = customerRepository.findAll(new PremiumCustomerSpec());

// Ardalis.Specification (C#) describes criteria declaratively — with neither method

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.

Java
// For overdue to have and()/or()/not(), the Specification interface
// must provide them — simplest as default methods:
interface Specification<T> {
    boolean isSatisfiedBy(T candidate);

    default Specification<T> and(Specification<T> o) { return c -> isSatisfiedBy(c) && o.isSatisfiedBy(c); }
    default Specification<T> or (Specification<T> o) { return c -> isSatisfiedBy(c) || o.isSatisfiedBy(c); }
    default Specification<T> not()                   { return c -> !isSatisfiedBy(c); }
}

// then every rule — even a lambda — composes out of the box:
Specification<Invoice> overdue       = i -> i.getDaysLate() > 30;
Specification<Invoice> noticesSent   = i -> i.noticesSent();
Specification<Invoice> inCollections = i -> i.inCollections();

// Composition with AND / OR / NOT operators:
Specification<Invoice> toCollections =
        overdue
            .and(noticesSent)
            .and(inCollections.not());   // overdue AND notices sent AND NOT in collections

boolean send = toCollections.isSatisfiedBy(invoice);

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.

Java
// A plain predicate ALSO composes:
Predicate<Customer> premium = c -> c.getBalance() > 100;
Predicate<Customer> active  = Customer::isActive;
Predicate<Customer> target  = premium.and(active).or(vip).negate();
// ...but a predicate has no name or place in the domain model — a specification does

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, and Not methods. 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:

ApproachWhat it isKey difference from a specification
Plain if conditiona condition at the point of usehard to reuse and to test in isolation
Predicate / lambdaa function returning true or falsealso composes, but does not gather domain knowledge in one named place
Strategy patternchoosing how to perform an actionanswers "how", not "yes or no"
Rule enginea large system with its own languagefar 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 if is 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.

It is worth remembering one nuance: when a framework "provides specifications", it does not always mean the classic pattern. The specifications mechanism in Spring Data JPA is really an implementation inspired by the pattern, focused on building queries through the Criteria API — close to the idea, but not identical to the classic specification based on 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.

Sources

  • Wikipedia — Specification pattern — link
  • Enterprise Craftsmanship (Vladimir Khorikov) — Specification Pattern: C# implementation — link
  • DevIQ — Specification Pattern — link
  • Spring Data JPA — Specifications (documentation) — link
Share this insight