When you use a composite Specification purely for validation (e.g., rejecting an invalid domain object before it's saved), how do you get a useful, itemized error message instead of just a single true/false result?
answer
- boolean AND loses which-child-failed info
- unsatisfiedReasons/Violation list
- derive boolean+message from one check to avoid drift
- Bean Validation ConstraintViolation as real precedent
basics
~20 sA single yes/no doesn't tell the user WHY something failed. So instead of just isSatisfiedBy returning false, you add a way for each sub-rule to report its own failure reason, and collect all of them into a list the user can actually read.
solid answer
~40 sA bare `isSatisfiedBy(candidate): boolean` loses information the moment a composite AND fails - you know *that* it failed, not *which* leaf specification caused it or why. The fix is extending the contract beyond a boolean: each specification (leaf and composite) also exposes something like `unsatisfiedReasons(candidate): List<String>` or throws a structured exception carrying a violation code/message, and composites implement this by recursively collecting reasons from whichever children failed rather than short-circuiting to a single boolean. The trade-off is added interface surface and the temptation for reason strings to drift out of sync with the actual boolean logic if someone updates one without the other - so many implementations derive both from the exact same underlying check to avoid duplication.
go deeper
Should recognize that a plain yes/no isn't very helpful when a user needs to know what to fix, without needing to design the mechanism.
Should propose a reasonable mechanism (e.g., a list of failure messages) and understand that AND-composites should collect reasons from every failing child.
Should design the itemized-reason contract to derive from the same underlying check as the boolean to avoid drift, and correctly reason about how OR-composites should report differently from AND-composites.
Should recognize this as one instance of a broader validation-design decision (citing precedent like Bean Validation), and judge when the added interface surface is and isn't worth requiring across a codebase's specifications.
## Where a bare boolean destroys information The problem starts with the minimal Specification contract itself: `isSatisfiedBy(candidate): boolean`. That's sufficient for validation ("is this valid, yes or no") and for selection ("does this belong in the result set"), but it's information-destroying the instant you compose specifications with AND. If `bigOrderSpec.and(inStockSpec).and(noFraudFlagSpec)` returns false for a given order, the caller learns only that at least one of the three sub-rules failed — not which one, and not why. - For an internal filtering operation that's often fine; nobody needs to know why a row didn't match a search filter. - But for validation surfaced to an end user or a caller who needs to fix the input — "why was my order rejected?" — a bare boolean is close to useless, and forces whatever code called `isSatisfiedBy` to either give a generic, unhelpful error ("invalid order") or reimplement the same checks a second time, redundantly, just to figure out which one actually failed — reintroducing the exact duplication-of-business-logic problem the Specification pattern exists to prevent in the first place. ## The standard fix The standard fix is to extend the Specification's interface beyond a single boolean method. A common approach: each specification also implements something like `unsatisfiedReasons(candidate): List<Violation>` (where `Violation` might carry a code and human-readable message), or the pattern is inverted so that instead of returning a boolean, `isSatisfiedBy` throws a structured exception carrying the specific violation when it fails, which callers can catch and inspect. For composites, the implementation is recursive by construction: - An `AndSpecification.unsatisfiedReasons(candidate)` calls `unsatisfiedReasons` on both children and concatenates whatever non-empty lists come back (an `AndSpecification` fails if *either* child fails, so you want to know about both, not stop at the first). - An `OrSpecification`, by contrast, is trickier to report meaningfully — if `orderSpec.or(vipOverrideSpec)` fails, the honest report is "neither alternative was satisfied," and a good implementation reports both children's reasons together rather than picking one arbitrarily, since either one becoming true would have made the OR succeed. ## Why it is worth designing deliberately Why is this worth designing deliberately rather than bolting on later? Because the naive approach — keeping isSatisfiedBy as the sole source of truth and separately writing message-generation code that mirrors its logic — reintroduces **drift risk**: someone tweaks the boolean condition in `isSatisfiedBy` (say, changing a threshold from $50 to $75) and forgets to update the parallel message-generation code, so the system now rejects an order correctly but tells the user the wrong reason, or accepts an order and shows a leftover "must exceed $50" message from a stale code path. The disciplined version derives both the boolean and the reason from the *same* underlying check — e.g., a single method returns a structured result object (`{ satisfied: boolean, reason: string? }`) that both `isSatisfiedBy` and `unsatisfiedReasons` delegate to internally — so there's exactly one place the actual comparison logic and its accompanying message live together, and a threshold change can't produce a boolean/message mismatch because there's only one line of code computing both. ## A real-world precedent A well-known real-world instance of this exact problem is Java's Bean Validation (JSR 380 / Jakarta Validation, `@NotNull`, `@Size`, etc.) — conceptually a validation-flavored specification framework where each constraint annotation is a leaf-level rule, and the validator engine, rather than returning a single pass/fail boolean for an object, returns a `Set<ConstraintViolation<T>>` — the itemized, composite equivalent of `unsatisfiedReasons` — specifically because a single boolean was recognized early on as inadequate for the actual use case (showing a user which of several fields on a form are wrong). Custom domain specifications solving the same problem for richer business rules (not just field-level constraints) face the identical design question and the identical answer: 1. itemize failures at the leaf level; 2. aggregate them at the composite level; 3. derive the reason from the same logic that derives the boolean so the two can't silently diverge. ## The trade-off The trade-off to name explicitly: this expanded contract adds real interface surface (every specification implementation now needs a reason-generation path, not just a boolean check), and for specifications that are purely internal filtering tools with no user-facing failure message ever required, it's unnecessary ceremony — so, consistent with the broader "when not to use it" judgment call, teams typically implement the itemized-reason contract only on the validation-facing specifications that actually need to explain themselves to a human, not uniformly across every specification in the codebase.
- How should an OrSpecification report failure reasons differently from an AndSpecification?An AndSpecification fails if any child fails, so it should aggregate reasons from all failing children since every one of them is a genuine blocker. An OrSpecification only fails if every child fails, so a meaningful report needs to communicate that none of the alternatives were satisfied, typically by including all children's reasons together so the user understands every alternative path they could have taken.
- What's the risk of maintaining isSatisfiedBy's boolean logic and a separate error-message-generation function independently?They can drift apart - someone updates the threshold or condition in one but forgets the other, producing a system that behaves correctly (accepts/rejects properly) but reports an inconsistent or stale reason to the user, which erodes trust in the error message and can mislead support staff debugging the report.
- Is it worth adding an itemized-reason contract to every specification in a codebase, even ones used only for internal filtering?No - the extra reason-generation method is only worth its added interface surface for specifications whose failures are actually surfaced to a human who needs to act on them, like form validation. A specification used purely to filter a search result set has no audience for 'why didn't this row match,' so keeping it to a plain boolean isSatisfiedBy avoids unnecessary implementation cost.
A single red X on a form tells you something's wrong; a form that circles the exact fields with a note under each ('email invalid,' 'password too short') tells you what to fix. Composite specifications need the second kind of feedback, not just the red X.
saying these in an interview costs you the question
- Thinks a single isSatisfiedBy boolean is always sufficient for user-facing validation
- Reimplements the same condition twice (once for the boolean, once for the message) instead of deriving both from one source
- Can't explain how AND vs OR composites should aggregate failure reasons differently
- Unaware of a real precedent like Bean Validation's itemized ConstraintViolation set
- Adds reason-reporting machinery uniformly even to purely internal, non-user-facing filtering specifications