When a submitted request breaks a dozen rules at once, do you return the first failure or all of them in one response? What are the consequences of each choice for the caller and for the server?
answer
- aggregate within a phase, stop between phases
- fail-fast = one round trip per mistake
- cheap pure checks first, I/O checks last
- cap count, deterministic order
- enumeration risk survives aggregation
basics
~20 sAggregate: run all cheap validators and return every violation in one array, so a form can be fixed in one pass instead of one round trip per mistake. Fail fast only across layers - parse errors stop everything, and expensive or stateful checks run only after cheap ones pass.
solid answer
~40 sDefault to aggregating. Fail-fast forces the user through as many submit-and-correct cycles as they have mistakes, which is a genuinely bad experience and multiplies traffic. Bean-Validation-style frameworks collect all constraint violations for exactly this reason. Aggregation is not unconditional though. Validation is layered: parse, then schema and per-field constraints, then cross-field rules, then stateful checks that hit the database or another service. If a layer fails, running the next is meaningless or unsafe, so I aggregate within a layer and stop between layers. That also caps cost - I do not want an attacker triggering twenty uniqueness lookups per request. Operationally I keep ordering deterministic, cap the array length, and never let one aggregate response disclose more than a single check would - existence checks such as email-already-registered stay generic on public signup paths.
go deeper
Say you return all of them at once so the user fixes the form in one go, and that frameworks collect violations for you.
Add the layering argument: aggregate within a phase, stop before phases whose inputs are already invalid or that cost I/O.
Bring in cost control, determinism, disclosure risk, and the split between atomic rejection and per-item bulk results.
Set it as policy - which phases exist, what may be aggregated, hard caps - so behaviour is uniform across services and load from validation is predictable.
## The default is collect-all A registration form with six bad fields should produce six violations in one response. Fail-fast turns that into six requests, six spinners, and a user who fixes the password rule only to discover the postcode rule. Every mainstream validation framework - Jakarta Bean Validation, JSON Schema validators, most serializer libraries - is built to accumulate rather than throw on first failure, and the per-field array shape exists precisely to carry the accumulation. ## Why you cannot aggregate everything Validation happens in phases, and later phases presuppose earlier ones. 1. **Parse / framing** - the bytes must become a document. If JSON is truncated there is nothing to validate; you emit one error and stop. 2. **Structure and type** - required properties, types, enum membership. A field that is a string where an object was expected cannot then be checked for its inner rules. 3. **Per-field constraints** - length, range, pattern, format. Cheap, pure, fully aggregatable. 4. **Cross-field rules** - endDate after startDate, discount requires a coupon code. Only meaningful once both fields individually parsed; aggregate within this phase. 5. **Stateful checks** - uniqueness, referenced entity exists, quota remaining. These cost I/O. The practical rule: aggregate greedily inside a phase, stop at the first phase that produced violations. That gives the caller everything knowable at that point without running checks whose inputs are already known bad. ## Cost and abuse Stateful checks turn one request into N queries or N calls to a neighbouring service. An unauthenticated endpoint that aggregates across a large array can be used to amplify load. Cap the number of collected violations (a few dozen), cap the size of accepted arrays, and keep expensive checks behind the cheap gate. ## Determinism If your validators run in hash order, the same bad request produces differently ordered arrays on different runs, which makes snapshot tests flaky and confuses support. Sort deterministically - document order, or by target path - and treat the order as presentation only, never as meaning. ## Aggregation and information disclosure Collecting violations can reveal more than intended. On a public signup, reporting email-already-in-use is an account-enumeration oracle; batching it with other errors does not make it safer. Keep security-sensitive checks out of the detailed array and answer generically, regardless of how much else you aggregate. ## Partial success is a different question For bulk endpoints, teams sometimes want to accept the good items and report the bad ones. That is a deliberate contract choice with real consequences - the whole request is no longer atomic - and it needs a per-item result structure rather than a validation error array. If you keep the request atomic, the aggregate array is still the right shape, with each entry pointing at the failing item by index. ## What to say in the interview Default to aggregate; justify it by round trips and UX; then show you know it is layered, bounded, deterministic, and constrained by what is safe to disclose.
- Should the violation array have a guaranteed order?It should be deterministic but not semantically meaningful. Sorting by the payload path or by document order keeps tests and support diagnoses stable across runs. Clients must not infer priority from position; if some violation is special, mark it with a field rather than relying on it being first.
- A bulk endpoint receives 500 items and 3 are invalid. What do you return?It depends whether the operation is atomic. If it is, reject the whole request and return three violations whose targets point at the failing indices. If you deliberately support partial success, the response is no longer a validation error at all - it is a per-item result document listing accepted and rejected items, and the status reflects that the request was processed.
saying these in an interview costs you the question
- Throwing on the first constraint violation because it is simpler, with no thought for the caller
- Running database uniqueness checks for every field before the cheap syntactic checks pass
- Returning an unbounded violation array for a huge bulk payload
- Assuming aggregation makes enumeration-sensitive checks safe to report in detail