Invariants and where to enforce them
An invariant is a condition that must always hold for an object to be valid: balance >= 0, loan.dueDate > loan.startDate. Corrupt state is worse than a thrown exception — design so invalid objects cannot exist.
| Layer | Validates | Example |
|---|---|---|
| Constructor | Object creation rules | Money rejects negative amount |
| Entity method | State transition rules | withdraw() checks sufficient funds |
| Service | Cross-entity rules | borrow limit across all active loans |
| API boundary | Input format | null checks, range validation |
Fail-fast vs Result types
Exceptions for truly exceptional paths (out of stock, network down). Result/Either (Optional, Result<T,E>) for expected failures callers should handle (insufficient funds, duplicate reservation). Avoid boolean soup: if (!withdraw()) tells the caller nothing — prefer InsufficientFundsException or Result.failure(REASON).
Null-object and defensive design
NullNotificationChannel that no-ops instead of null checks everywhere — use sparingly, only when "do nothing" is a valid policy. Prefer Optional for absent values. Don't return null from public APIs without documenting contract.
Senior-level signal
For BankAccount.withdraw(amount): "I enforce amount > 0 in the method, check balance here (entity invariant), and let the service coordinate distributed lock if accounts span shards — out of scope today." State which exceptions are checked vs unchecked and why. Mention concurrent withdraw: optimistic versioning or synchronized block — pick one.
Aggregate consistency
In DDD terms, one aggregate (e.g., BankAccount) owns its invariants; cross-aggregate rules belong in an application service. In LLD, draw the boundary: don't let Loan directly mutate BookItem.status — route through LibraryService.
Where this goes next
You've completed the LLD reference arc — revisit SOLID Principles when reviewing designs, and practice full timed rounds on Parking Lot or LRU Cache to integrate process, patterns, and API discipline.
Further Reading
- Effective Java — Item 69: Use exceptions only for exceptional conditions; Item 70: Use checked exceptions for recoverable conditionsBook20m
- Martin Fowler — Fail Fast (validate early, don't propagate corrupt state)Article10m
- Domain-Driven Design Distilled — Ch. 3: Be Mindful of Bounded Contexts (§on invariants and aggregate consistency boundaries — interview-depth skim)Book20m
Practice Tasks (Optional)
Design or implement locally in any language — no autograding. Focus on class structure, extensibility, and being able to explain trade-offs out loud.
- Design BankAccount with invariants25m
withdraw(amount) must not allow negative balance. Where do you enforce: constructor, withdraw(), or separate validator? Handle concurrent withdraw attempts conceptually. Write method signatures and thrown exceptions.