Specification vs Repository is not a contest where one pattern replaces the others. The terms describe different responsibilities, and confusion starts when a single class is expected to name a business rule, construct a database query, hide persistence, validate input, and decide authorization. This comparison keeps those jobs separate so you can recognize the smallest abstraction that fits.
The short version is simple. A Specification names criteria. A Repository mediates access to persisted domain objects. A Query Object represents a database query. They can collaborate, but they do not answer the same question.
Start With the Question Each Abstraction Answers
The historical Specification described by Eric Evans and Martin Fowler is an encapsulated predicate. It answers whether a candidate satisfies a condition. That condition might support selection, validation-adjacent checks, or describing what needs to be built. The important part is the named criterion.
Fowler's Repository description gives Repository a different job. It mediates between the domain and data-mapping layers through a collection-like interface. A client can submit criteria, including a Specification, but the Repository owns the persistence-facing access boundary.
Fowler's Query Object is narrower in another direction. It represents a database query and translates object-oriented criteria into a query language. Its reason to exist is retrieval, not general domain truth.
Here is the first decision table:
| Abstraction | Primary question | Typical output | Natural home |
|---|---|---|---|
| Specification | Does this candidate satisfy named criteria? | bool, predicate, or criteria expression |
Domain or application code |
| Repository | How does the application access persisted objects through a controlled boundary? | Entities, projections, counts, or operation results | Infrastructure behind an application-facing interface |
| Query Object | What database query represents this use case? | A projected result, page, scalar, or query model | Application or data-access code |
These descriptions are intentionally bounded. Modern libraries sometimes call a full query envelope a "specification." Such an object may contain criteria, ordering, includes, projection, pagination, tracking flags, and tags. That is valid package terminology, but architecturally it behaves much more like a named query description than the historical predicate-only pattern.
Small Interface Sketches Make the Boundary Visible
You do not need a framework to see the difference. These interfaces are sketches, not implementation prescriptions:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace ComparisonSketches;
public interface IDomainSpecification<in T>
{
bool IsSatisfiedBy(T candidate);
}
public interface IExpressionSpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
}
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken);
Task<IReadOnlyList<T>> ListAsync(
IExpressionSpecification<T> specification,
CancellationToken cancellationToken);
}
The Specification carries criteria. The Repository accepts criteria and controls execution. Neither interface says that every query must use a Repository, nor that a Specification must know how data is stored.
A Query Object can instead own one use-case-shaped result:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace ComparisonSketches;
public interface IQueryObject<TResult>
{
Task<TResult> ExecuteAsync(
CancellationToken cancellationToken);
}
public sealed record CustomerSummary(
Guid Id,
string DisplayName);
public sealed record CustomerSummaryPage(
IReadOnlyList<CustomerSummary> Items,
int TotalCount);
This shape makes a different tradeoff. The query contract directly expresses the result needed by the caller. It does not pretend that every retrieval operation is a collection operation over domain entities.
Specification vs Repository
The Specification vs Repository distinction becomes clear when you separate criteria from execution.
A pure Specification can be evaluated against an in-memory object. An expression-based Specification can expose provider-visible criteria. In both cases, its central meaning is "these conditions define a match."
A Repository coordinates access to persisted objects. It may apply a Specification, add mandatory tenant constraints, materialize results, save changes, or expose use-case-specific methods. The Repository boundary is also where you can prevent callers from depending on provider-specific IQueryable<T> behavior.
| Choose this emphasis | Benefits | Costs and risks |
|---|---|---|
| Specification without Repository | Keeps a named rule reusable and avoids adding a persistence facade when direct data access is already appropriate | Execution policy remains with the caller or another service |
| Repository accepting Specifications | Centralizes persistence access and keeps provider execution behind an interface | Can become a generic catch-all with weak, overly broad methods |
| Repository with explicit use-case methods | Makes important operations discoverable and constrains query behavior | More methods and more interface maintenance |
Repository does not become unnecessary just because a Specification exists. Specification also does not become mandatory because a Repository exists. The combination is useful when several callers need named criteria while persistence execution must remain controlled.
Be cautious with a generic method such as ListAsync(ISpecification<T>) if the application needs many unrelated projections and result shapes. The method may accumulate hidden conventions around includes, paging, tracking, and authorization. At that point, a use-case-specific Query Object can communicate more clearly.
Specification vs Query Object
A Specification and a Query Object overlap when both contain filter criteria. Their intent is still different.
A Specification is strongest when the criterion has a stable name in the problem domain: an account is eligible for renewal, an order requires review, or a candidate matches a search boundary. That rule may be reused in multiple decisions.
A Query Object is strongest when the retrieval operation is the concept: load an account dashboard, return a paged support queue, or calculate a monthly summary. Projection, sorting, grouping, pagination, and database-specific choices are often part of that use case.
| Design signal | Specification | Query Object |
|---|---|---|
| Named Boolean condition is the main concept | Strong fit | Possible but broader than needed |
| Exact result shape is the main concept | Limited fit | Strong fit |
| Reuse in an in-memory domain decision | Strong fit | Weak fit |
| Joins, grouping, projection, and paging define the operation | A full query envelope can carry them, but meaning becomes broader | Natural fit |
| Caller should receive one purpose-built result | Possible through projection | Natural fit |
A modern query specification can reasonably be described as a specialized Query Object. The label matters less than an honest contract. If the object carries query-shaping state, document it as a query description. Do not market it as a pure business rule simply because it has "Specification" in the class name.
A Predicate Is Not Automatically a Specification
.NET defines Predicate<T> as a delegate that returns a Boolean value for an object. A lambda such as customer => customer.IsActive is therefore a predicate.
A Specification adds identity and intent around a predicate. ActiveCustomerSpecification gives the condition a name, a reusable type, and a place for domain-focused tests. It may also support composition or descriptive metadata.
That additional structure is not automatically valuable. If a predicate appears once, is obvious in context, and has no independent business meaning, an inline lambda can be clearer. Creating a class for every Where clause produces ceremony without necessarily improving the design.
Use a Specification when naming and reuse earn their keep. Use a plain predicate when the condition is local and uncomplicated.
Specification Is Not a Gang of Four Pattern
This point needs a direct answer: Specification is not one of the Gang of Four's 23 design patterns.
The publisher's page for Design Patterns: Elements of Reusable Object-Oriented Software describes the 1994 catalog of creational, structural, and behavioral patterns. Specification is not in that catalog. Its well-known lineage comes from domain-driven design and enterprise application design, including the Evans/Fowler material.
Why does the confusion persist? A specification implementation can use GoF patterns internally:
- Composite can represent trees of AND, OR, and NOT criteria.
- Strategy can swap the algorithm used to evaluate or translate criteria.
Using a GoF pattern to implement part of another design does not add that design to the GoF catalog. If you want broader context for the original catalog, the Big List of Design Patterns provides a useful map.
Specification vs Strategy and Composite
Strategy and Composite deserve bounded comparisons because their structures can appear in Specification code.
Strategy answers how an operation should be performed. A tax calculation strategy, compression strategy, or routing strategy provides an interchangeable algorithm. Specification answers whether criteria are satisfied.
Composite organizes individual objects and groups through a uniform interface. An AndSpecification can be a composite node containing child specifications. Composite explains that tree structure. Specification explains the predicate meaning.
| Pattern | Core intent | Example question |
|---|---|---|
| Specification | Express named criteria | Is this order eligible for automatic approval? |
| Strategy | Select interchangeable behavior | Which approval algorithm should process this order? |
| Composite | Treat leaves and groups uniformly | Can one evaluator traverse atomic and combined criteria the same way? |
These patterns can collaborate. They should not be treated as synonyms.
Validation Is a Neighbor, Not a Replacement Name
The historical Specification literature includes validation as a use. That does not mean a Boolean Specification is a complete validation system.
A Boolean rule can answer, "Is the credit limit sufficient?" Real validation often needs more:
- A stable error code.
- A human-readable message.
- The field or domain member involved.
- Multiple failures in one response.
- Boundary-specific behavior for malformed input.
Microsoft's domain-model validation guidance keeps aggregate invariants in the domain model and discusses Specification plus Notification as an advanced option. That is a useful separation. Specifications can provide reusable rule predicates, while a notification or result adapter provides structured failures. The aggregate still enforces valid state transitions.
Request validation is another boundary. It answers whether incoming data is present, well-formed, and acceptable to process. A domain Specification should not be forced to parse transport formats or reproduce model-binding concerns.
Authorization Policy Is a Security Decision
A Specification may filter records that appear relevant to a user, but it is not an authorization policy.
ASP.NET Core policy-based authorization evaluates requirements through handlers against a user and, when needed, a resource. It owns security semantics such as authenticated identity, claims, requirements, success, and failure.
A data criterion such as document => document.OwnerId == userId can support row selection. It does not, by itself, prove that the current principal is allowed to read, modify, export, or disclose the document. Authorization must remain an independently enforced boundary.
This distinction is not academic. Query criteria can be omitted, combined incorrectly, or bypassed. Security decisions need explicit policy enforcement and tests that do not depend on a convenient Repository or Specification call happening first.
Rules Engines Solve a Larger Problem
A Specification normally represents deterministic criteria in code. A rules engine addresses a broader operational model.
The official Microsoft RulesEngine documentation describes workflows, multiple inputs, scoped parameters, result trees, actions, and rules supplied through external configuration. Those capabilities support scenarios where rules are authored, stored, grouped, evaluated, and acted upon through a runtime system.
| Need | Specification | Rules engine |
|---|---|---|
| Small named predicate in code | Strong fit | Usually excessive |
| Boolean reuse in domain decisions | Strong fit | Possible |
| External rule configuration | Not inherent | Strong fit |
| Workflow and action execution | Keep separate | Supported by engine model |
| Rich result tree across many rules | Requires a separate adapter | Natural capability |
| Operational governance for changing rules | Application responsibility | Often part of the platform design |
Do not move to a rules engine merely because several Specifications exist. The extra runtime, configuration, observability, and governance concerns are real. Conversely, do not stretch a set of Boolean objects into a home-grown rules platform when external authorship and workflow execution are actual requirements.
A Practical Selection Matrix
The most useful Specification vs Repository decision starts with the dominant responsibility:
| Situation | Prefer | Why |
|---|---|---|
| One local, readable filter | Inline predicate | The name and class add little value |
| Reusable domain condition | Pure Specification | The rule gains identity without persistence coupling |
| Reusable provider-visible criterion | Expression Specification | The provider can inspect the expression |
| Controlled persistence boundary over domain objects | Repository | Access and materialization stay behind a collection-like contract |
| Use-case-specific projection, grouping, or page | Query Object | The operation and result shape are explicit |
| Structured domain-rule failures | Specification plus result adapter | Boolean rule and failure reporting remain separate |
| User/resource access decision | Authorization policy | Security semantics belong to requirements and handlers |
| Externally configured workflows and actions | Rules engine | The problem exceeds a code-level predicate |
There is no universal winner. The tradeoff is clarity against ceremony. Each abstraction should remove ambiguity from the responsibility it owns. If it merely moves a short expression into another file, it has not earned much.
Frequently Asked Questions
Can a Repository execute a Specification?
Yes. Fowler's Repository description explicitly allows declarative query specifications to be submitted to a Repository. The Specification supplies criteria, while the Repository controls persistence access and execution.
Can I use Specifications without a Repository?
Yes. A pure Specification can run in memory, and an expression Specification can be applied directly by application or data-access code. Repository is an independent architectural choice.
Is a query specification just a Query Object?
Often it is a specialized form of Query Object, especially when it carries ordering, projection, includes, and pagination. A predicate-only domain Specification is narrower and has a different center of gravity.
Is Specification a behavioral GoF pattern?
No. Specification is not one of the GoF 23. It may use Strategy or Composite internally, but that implementation relationship does not change its historical catalog.
Should validation return only true or false?
Not when callers need actionable failures. Keep the reusable Boolean rule in a Specification if it helps, then adapt failed rules into structured codes and messages. Request validation and aggregate invariant enforcement remain separate.
Can a Specification enforce authorization?
It can contribute criteria, but it should not be the authorization boundary. Use authorization policies and handlers to evaluate the current user, requirements, and resource access.
Choose by Responsibility, Not by Pattern Count
For Specification vs Repository, the clean decision is not "which pattern is better?" Ask what needs a name, what needs execution, and what needs a boundary.
Use a Specification for meaningful criteria. Use a Repository when persistence access needs mediation. Use a Query Object when the retrieval operation and result shape are the concept. Keep validation, invariants, authorization, and rules-engine concerns in their own contracts.
That approach may use fewer patterns, or it may use several together. Either outcome is fine. The goal is a design where each abstraction tells the truth about the responsibility it owns.

