skip to content

As a tech lead reviewing a design that proposes composable Specification objects for every filtering and validation need across a large codebase, what trade-offs and failure modes would make you push back, and when is a simpler predicate/query-object approach the better call?

level: principalimportance: should knowfreq 35%

answer

  1. Specification-itis: one-off wraps
  2. deep unnamed composite trees hurt debuggability
  3. query-safe vs validation-only discipline
  4. ask: reused? composed? queried?
  5. richer rulebook domains earn the pattern

basics

~20 s

Specifications add extra classes and indirection. If a team uses them for every tiny check, the codebase gets harder to read and navigate for little benefit. Use them where rules are genuinely reused, combined, or need to become queries - not everywhere.

solid answer

~40 s

The pattern's cost is real: extra classes, an extra layer between "what's the rule" and "where's it written," and (if pushed to repositories) the query-translation subset limitation and NULL-semantics traps. I'd push back when specifications are being introduced for rules used in exactly one place with no reuse, composition, or query-translation need - a plain predicate/lambda or an inline condition is more direct there. I'd also flag deep, unnamed composite trees that hurt debuggability, and specifications whose isSatisfiedBy logic can't actually be translated to a query being used for repository-level "selection" anyway, silently forcing full-table loads. The pattern earns its complexity specifically for genuinely shared, composed, or query-bound business rules - not as a default for every conditional.

go deeper

for a junior

Not typically expected to drive this call, but should be able to recognize when a review comment says 'this doesn't need a Specification class' and understand the reasoning when it's explained.

for a middle

Should be able to apply a basic heuristic (is this rule reused or combined?) to decide for or against introducing a Specification on a single PR.

for a senior

Should proactively flag Specification-itis and untranslatable query specifications in code review, and know concrete mitigations (explainability methods, query-safe labeling).

for a principal

Should set explicit team-level design guidance and review heuristics for when the pattern is and isn't warranted, weigh it against the domain's actual rule-richness, and be able to justify pushing back on a large-scale proposal to use it as a default.

## What the abstraction actually costs At the architectural-review level, the Specification pattern's cost isn't abstract — it's measured in the same currency as any abstraction: - extra indirection a reader has to traverse to understand what a piece of code actually checks; - extra files and classes to navigate; - extra design surface (interfaces, combinators, factory wiring) that has to be learned by every engineer who touches the domain layer. That cost is worth paying when it buys back something concrete: - reuse across multiple call sites; - composability of independently meaningful business rules; - or the ability to reuse the exact same rule for both in-memory validation and repository-level query filtering. When a proposed specification buys none of those — a rule used in exactly one place, never combined with anything, never queried against — the abstraction is pure overhead, and I'd push the team toward a plain predicate, a private method, or (in languages with first-class functions) a lambda passed directly to a `filter` call. This is the single most common failure mode I'd flag in review: **"Specification-itis,"** where a team internalizes "DDD says wrap business rules in Specifications" as a blanket rule rather than a tool for specific situations, and ends up with dozens of one-line, single-use Specification classes that add navigation cost without adding a single byte of reuse value. ## Failure mode: compositional opacity at scale A second failure mode is compositional opacity at scale. Composable specifications are genuinely powerful for building complex eligibility rules from small primitives, but nothing about the pattern forces anyone to keep the resulting trees shallow or well-named. I've seen production incidents traced to a five-or-six-level-deep and/or/not tree built dynamically from configuration, where the only way to figure out which specific sub-rule caused a false result was to attach a debugger and step through composite after composite — because no one had built an explain()/failure-reason mechanism, and the tree's structure existed only implicitly in how the composition code was wired at startup. In review, I'd require that any composite specification beyond two or three levels deep either: - gets intermediate variables with domain names, so the tree structure is visible in source, not just in runtime object graphs; - or gets a reason-reporting mechanism so failures are diagnosable without a debugger session in production. ## Failure mode: straddling the validation/query boundary A third, and in my experience the most costly failure mode, is letting specifications straddle the validation/query boundary without discipline. As covered in the query-translation trade-off, not every isSatisfiedBy expression is mechanically translatable to SQL, and the mismatch between imperative in-memory logic and declarative query logic (including SQL's three-valued NULL semantics) means a specification that looks like a clean, reusable "source of truth" for both validation and selection can, in practice, either: - throw at query-translation time; - silently fall back to a full-table load; - or — worst of all — silently diverge in behavior between the two paths on edge-case (NULL-heavy) data. As a reviewer, I'd insist any specification used for repository-level selection be restricted to a genuinely declarative, query-translatable subset, be covered by an integration test against a real database (not just an in-memory fixture test), and be explicitly labeled if it's validation-only and must never be handed to a repository as a filter. ## Where the pattern genuinely earns its keep Beyond these mechanical failure modes, there's a genuine architectural trade-off worth naming explicitly to the team: Specification adds most value in domains with a rich, evolving rulebook that's consulted from multiple angles — - insurance underwriting; - loan eligibility; - promotions/pricing engines; - access-control policy evaluation — where the business rules themselves are a first-class part of the domain model that product owners and domain experts actually discuss and change independently of the surrounding workflow code. In more CRUD-shaped domains, where "business rules" are really just a handful of straightforward validations tightly coupled to one specific form or one specific query, the pattern's composability and reuse machinery mostly goes unused, and a repository method with a well-named parameter list, or a simple validator function, does the same job with less ceremony and a shallower learning curve for new team members. ## My practical review heuristic My practical review heuristic: I ask "how many distinct call sites will actually consume this rule, and will any of them need to combine it with another rule or translate it into a query?" | What comes back | The call | |---|---| | The honest answer is one call site, no combination, and no query need | I push back toward a plain predicate | | The answer is multiple call sites, real combination, or a genuine query-translation need | Specification is the right tool | And at that point I also insist on the guardrails above: - shallow, named composition; - explainability for composite failures; - an explicit line between query-safe and validation-only specifications, so the pattern's genuine value isn't undermined by its own failure modes as the codebase grows.

  • A junior engineer argues 'DDD says we should always use Specifications for business rules' - how do you respond in review?
    I'd clarify that Evans and Fowler describe Specification as a tool for rules that are reused, composed, or need to become queries - not a mandate to wrap every conditional. I'd ask how many call sites this specific rule has and whether it's ever combined with another rule; if the answer is 'one, never,' a plain method or predicate is the better, more direct choice.
  • How would you retrofit explainability into an existing deep composite specification tree without a large rewrite?
    Add an optional method (e.g., unsatisfiedReasons(candidate)) to the base Specification interface that composites implement by recursively collecting reasons from any sub-specification that returned false, defaulting to a generic message for leaves that don't implement it yet - this can be layered in incrementally, leaf by leaf, rather than requiring every existing specification to be rewritten at once.
  • What's a concrete signal in production metrics or incidents that a team has over-applied query-translatable specifications?
    A pattern of slow endpoint response times or memory spikes tracing back to repository calls that load full tables before filtering, combined with specification classes whose isSatisfiedBy logic includes constructs (loops, external calls, custom methods) that can't be expressed in the ORM's query builder - the tell is specifications that were designed for in-memory validation being reused for repository-level selection without anyone checking whether they're actually query-translatable.

It's like building custom scaffolding around a single brick you're about to lay - fine if you're going to reuse that scaffolding for a hundred more bricks in a complex wall, wasteful if you only ever needed to place one brick and could've just set it by hand.

saying these in an interview costs you the question

  • Treats Specification as mandatory for every business rule regardless of reuse or composition
  • Has no answer for how to debug a failing deep composite tree in production
  • Approves specifications for repository filtering without checking query-translatability first
  • Can't articulate a concrete heuristic for when NOT to use the pattern
  • Conflates 'more design pattern usage' with 'better architecture' without weighing the reader's navigation cost

context