IMPLEMENTATION QUALITY / OBJECT DESIGN BRIEF

Validation boundaries

Validation boundaries define where and how input is checked.

IntermediatePhase 07 / Topic 2 of 8ResponsibilitiesCollaborationsExtensibility
01

Overview

Validation boundaries define where and how input is checked. Untrusted data (HTTP requests, files, messages) is validated at the system edge for shape and format; domain objects enforce business invariants in their constructors and methods; and the database enforces final constraints. Each layer checks what it is responsible for.

The 'parse, don't validate' principle says convert raw input into typed, valid objects once at the boundary (an Email value object, a Quantity greater than zero). After that, the rest of the code can trust its inputs instead of re-checking everywhere. Validation is also a security control: unvalidated input causes injection and logic flaws.

Airport security

Passports are checked at the border (edge validation), boarding passes at the gate (business rule), and seat numbers on the plane (final constraint). Each checkpoint verifies what it is responsible for, and once through security, passengers are not re-scanned at every shop.

02

When to use it

  • Accepting any external input.
  • Designing constructors of domain objects.
  • Preventing duplicated or missing checks across layers.
03

Where it shows up in interviews

Input handling

Recognize it when: API or CLI accepting user input.

  • Design a URL shortener API
  • Design a parking lot entry
Always-valid domain

Recognize it when: objects must never be invalid.

  • Design a bank account
  • Design an order
04

Where it is used in real software

Zod, Joi, and Bean Validation

Schema validation libraries check request shape at the edge (z.object, @NotNull, @Size).

OWASP input validation

Allow-list validation at trust boundaries is a core defense against injection.

Database constraints

NOT NULL, UNIQUE, CHECK, and foreign keys are the last line of defense.

05

Key terms

Trust boundary
Where data enters from an untrusted source.
Syntactic validation
Format and type: is this an email-shaped string?
Semantic validation
Business meaning: is this email already registered?
Parse, don't validate
Convert input into types that guarantee validity.
Invariant
Rule an object always satisfies.
06

How it works, step by step

  1. 1
    Validate shape at the edge

    Types, required fields, lengths, formats (allow-lists).

  2. 2
    Parse into value objects

    Email, Money, Quantity.

  3. 3
    Enforce invariants in domain objects

    Constructors and methods reject invalid states.

  4. 4
    Check business rules in services

    Uniqueness, availability, permissions.

  5. 5
    Back up with database constraints

    Unique indexes, checks.

07

Where each check belongs

POST /accounts/{id}/withdraw { amount }

Step 1 / 5
CheckLayerExample
amount is a number, requiredEdge schemaZod / Bean Validation
amount > 0, max 2 decimalsValue objectMoney.of(amount)
balance >= amountDomain entityaccount.withdraw(money)
caller owns the accountApplication serviceAuthorization check
balance never negativeDatabaseCHECK (balance >= 0)

NOWCheck: amount is a number, required | Layer: Edge schema | Example: Zod / Bean Validation

Each layer validates what it owns, so no check is missing or duplicated inconsistently.

08

Implementation

import { z } from "zod"; // Edge: shape and formatconst WithdrawBody = z.object({ amount: z.number().positive().max(10_000) }).strict(); // Value object: parsed once, always validclass Money {  private constructor(readonly cents: number) {}  static fromDecimal(amount: number) {    const cents = Math.round(amount * 100);    if (Math.abs(amount * 100 - cents) > 1e-6) throw new Error("At most 2 decimal places");    return new Money(cents);  }} // Domain: invariantsclass Account {  constructor(readonly id: string, readonly ownerId: string, private balanceCents: number) {}  withdraw(m: Money) {    if (m.cents > this.balanceCents) throw new Error("Insufficient funds");    this.balanceCents -= m.cents;  }} app.post("/accounts/:id/withdraw", async (req, res) => {  const parsed = WithdrawBody.safeParse(req.body);  if (!parsed.success) return res.status(400).json({ errors: parsed.error.flatten() });  const account = await accounts.get(req.params.id);  if (account.ownerId !== req.user.id) return res.status(403).end();   // authorization  account.withdraw(Money.fromDecimal(parsed.data.amount));  await accounts.save(account);  res.status(204).end();});
09

Complexity and performance

Edge validationO(input size)

Once per request.

Re-validationNone

Types carry guarantees.

10

Trade-offs

Defense in depth vs duplication

Checking in several layers catches more but can drift; give each layer distinct responsibilities.

Fail on first vs collect all

APIs for forms should return all field errors; domain objects usually fail fast.

11

Variants and related techniques

Schema-first APIs

OpenAPI or JSON Schema validates requests automatically.

Smart constructors

Factory functions returning a result for valid or invalid input.

12

Common mistakes

  • Validating only in the frontend.

    Fix: Clients can be bypassed; always validate on the server.

  • Deny-list validation.

    Fix: Use allow-lists for formats and characters.

  • Primitive strings flowing into the domain.

    Fix: Parse into value objects at the boundary.

13

Interview questions

Where should validation happen?

At trust boundaries for shape and format, in value objects and entities for invariants, in services for context-dependent business rules and authorization, and in the database for final constraints.

What does 'parse, don't validate' mean?

Instead of checking a string and passing it on as a string, convert it into a type that can only hold valid values, so downstream code never needs to re-check.

14

Practice problems

ProblemDifficultyWhat it trains
Validate a booking request end to endEasyLayered checks.
Design value objects for Quantity, Money, EmailMediumAlways-valid types.