Loading content...
You've spent days carefully designing a domain model. Classes are well-named, relationships are clear, behavior is properly encapsulated. The code is elegant. You're proud of it.
Then the product manager asks: "Can a premium customer split an order across multiple shipping addresses?" You realize there's no way to model that in your design. The elegant Order entity assumes one shipping address.
This is the design validation gap—the disconnect between what we think the design covers and what it actually covers. Closing this gap before implementation saves enormous rework costs. A design flaw discovered during coding costs 10x more to fix than one found during design review.
By the end of this page, you will have systematic techniques for validating that your domain model satisfies all requirements, methods for walking through scenarios with your design before writing code, and approaches for catching gaps in functionality, edge cases, and invariant enforcement early in the development process.
Developers often skip design validation because:
All three are costly illusions. Requirements are frequently misunderstood, implementation "fixes" create technical debt, and rushing past validation creates more deadline pressure downstream.
The economics:
| Gap Type | Example | Consequence if Missed |
|---|---|---|
| Missing entity | No Shipment entity when fulfillment workflow needs tracking | Major redesign mid-implementation |
| Missing relationship | No way to represent 'Order placed by Employee on behalf of Customer' | Workarounds that corrupt the model |
| Missing behavior | Order can't be split after being placed | Feature request becomes architecture change |
| Missing state | No 'Partially Shipped' status | Data hacks to represent state system doesn't model |
| Missing constraint | Nothing prevents negative inventory | Data corruption in production |
| Edge case gap | What if a customer has two active carts? | Undefined behavior, potential bugs |
The time spent validating design is typically 5-10% of total development time. The time spent fixing design flaws discovered later is typically 20-40% of total development time. Validation has one of the highest returns on investment of any software practice.
The most straightforward validation technique is traceability—mapping each requirement to the design elements that fulfill it. If any requirement lacks a corresponding design element, the design is incomplete.
Creating a traceability matrix:
123456789101112131415161718192021222324252627282930313233343536
# Requirements Traceability Matrix ## E-Commerce Order Management | Req ID | Requirement | Design Elements | Status ||--------|------------|-----------------|--------|| REQ-01 | Customer can add items to cart | `Cart.add_item()`, `CartItem` | ✅ Covered || REQ-02 | Customer can checkout cart | `Cart.checkout()` → creates `Order` | ✅ Covered || REQ-03 | Order must have shipping address | `Order._shipping_address: Address` | ✅ Covered || REQ-04 | Premium customers can split shipments | **??? NO COVERAGE** | ❌ GAP || REQ-05 | System tracks inventory levels | `Product._inventory_count` | ✅ Covered || REQ-06 | Cannot sell more than available | `Order.confirm()` checks inventory | ✅ Covered || REQ-07 | Order can be cancelled before shipping | `Order.cancel()` | ✅ Covered || REQ-08 | Refunds require manager approval | **??? NO COVERAGE** | ❌ GAP || REQ-09 | Customer sees order history | `CustomerRepository.find_orders()` | ✅ Covered || REQ-10 | Express shipping available | `ShippingMethod` enum includes EXPRESS | ✅ Covered | ## Identified Gaps ### REQ-04: Split Shipments**Problem**: Current `Order` has single `shipping_address`. Premium customers need multiple addresses for partial shipments. **Proposed Solution**: - Introduce `Shipment` entity (not just a status)- `Order` has many `Shipment`s- Each `Shipment` has its own address and `OrderItem`s- Add `Order.split_to_shipments()` method ### REQ-08: Refund Approval**Problem**: No approval workflow exists. `Order.refund()` currently executes immediately. **Proposed Solution**:- Introduce `RefundRequest` entity with PENDING/APPROVED/DENIED states- `Order.request_refund()` creates RefundRequest- `RefundRequest.approve(manager)` required before funds movementThe discipline:
For every requirement in your spec, you should be able to point to specific code that implements it. If you can't, either:
This process often reveals requirements that seem covered but actually aren't. "Order history" seems covered by storing orders, but does your design support querying by date range? By status? Pagination for customers with thousands of orders?
Traceability applies to non-functional requirements as well. If the system needs to 'handle 1000 concurrent users,' which design choices support that? If 'data must be encrypted at rest,' where is that addressed? These often get missed because they don't map to obvious entities.
Beyond static traceability, scenario walkthroughs validate that the design behaves correctly. You mentally (or collaboratively) execute user scenarios against your design, verifying that each step is possible and produces the right outcome.
The process:
This is essentially "running" your design before writing code.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
# Scenario Walkthrough: Customer Purchases with Promotion ## Actors- **Customer**: Sarah (premium member)- **System**: E-commerce platform ## Preconditions - Sarah is logged in (Session exists with customer_id)- Cart contains 3 items totaling $150- Active promotion: "20% off orders over $100" ## Scenario Steps ### Step 1: Sarah views cart| Action | cart = CartRepository.find_by_customer(sarah.id) ||--------|--------------------------------------------------|| Response | Cart with 3 CartItems, subtotal=$150 || Validation | ✅ Cart.subtotal() sums item prices correctly | ### Step 2: System applies promotions| Action | promotions = PromotionService.find_applicable(cart) ||--------|-----------------------------------------------------|| Response | List containing "20% off over $100" promotion || Question | ❓ How does PromotionService access cart details? || Answer | Via Cart.subtotal() and Cart.items - both public || Validation | ✅ Promotion eligibility can be checked | ### Step 3: Sarah proceeds to checkout| Action | order = cart.checkout(shipping_address, promotions) ||--------|------------------------------------------------------|| Response | New Order in PENDING state || Transformation | Cart → Order with OrderItems, promotions applied || Question | ❓ Does Order store promotions applied? || Answer | ⚠️ NO - Order only has final_total. Audit trail lost! || **GAP FOUND** | Need OrderPromotion join or promotion_code on Order | ### Step 4: Sarah enters payment| Action | order.add_payment_method(card_details) ||--------|----------------------------------------|| Response | PaymentMethod attached to Order || Question | ❓ Is this the right design? || Answer | ⚠️ CONCERN - Storing card in Order violates PCI || **GAP FOUND** | Should reference payment_token, not card details | ### Step 5: Sarah confirms order| Action | order.confirm() ||--------|-----------------|| Business Rules | Checks: inventory available, payment valid, address valid || State Change | Order.status → CONFIRMED || Side Effects | Inventory decremented, confirmation email triggered || Question | ❓ Who triggers the email? || Answer | ⚠️ UNCLEAR - Not in Order behavior || **GAP FOUND** | Need event/callback mechanism for side effects | ### Step 6: Order is fulfilled (later)| Action | order.ship(tracking_number) ||--------|----------------------------|| Precondition | status == CONFIRMED || State Change | status → SHIPPED, tracking stored || Validation | ✅ Covered in current design | ## Gap Summary1. **Order doesn't track which promotions were applied** - Need for audit trail2. **Payment storage violates PCI** - Need tokenization pattern3. **Side effects unclear** - Need domain events or hooksKey questions at each step:
Scenario walkthroughs are more effective with 2-3 people. Different perspectives catch different gaps. The person who designed the system often has blind spots; they unconsciously fill in missing pieces. Fresh eyes spot what's actually missing.
Invariants are conditions that must always be true for domain objects. Validating that your design enforces all required invariants prevents data corruption and undefined behavior.
Types of invariants:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
# Invariant Documentation and Verification class Order: """ INVARIANTS (must always be true): 1. Order must have at least one OrderItem - Enforced by: __init__ raises if items empty - Risk: Can items be removed after creation? Need remove_item guard. 2. Order.total == sum of OrderItem.subtotal (minus discounts) - Enforced by: total is calculated property, not stored - Risk: None if truly calculated. Verify no stored total field. 3. status transitions follow: PENDING → CONFIRMED → SHIPPED → DELIVERED - Enforced by: Each state change method checks current state - Risk: Is there a way to set status directly? Audit for set_status(). 4. Cancelled orders cannot transition to any other state - Enforced by: All transition methods check status != CANCELLED - Risk: Need to verify ALL methods include this check. 5. Shipped orders must have tracking_number - Enforced by: ship() requires tracking_number parameter - Risk: What if ship() is called with empty string? """ def __init__(self, order_id: str, items: list["OrderItem"]): # INVARIANT 1: Enforced at creation if not items: raise OrderException("Order must have at least one item") self._items = list(items) self._status = OrderStatus.PENDING self._tracking_number: str | None = None def remove_item(self, item_id: str) -> None: """Remove an item from the order.""" # INVARIANT 1: Must maintain at least one item if len(self._items) <= 1: raise OrderException("Cannot remove last item; cancel order instead") self._items = [i for i in self._items if i.item_id != item_id] @property def total(self) -> Money: # INVARIANT 2: Total is calculated, not stored (always consistent) return sum((item.subtotal for item in self._items), Money.zero()) def confirm(self) -> None: # INVARIANTS 3, 4: Transition guards self._require_status(OrderStatus.PENDING) self._status = OrderStatus.CONFIRMED def ship(self, tracking_number: str) -> None: # INVARIANT 3: Transition guard self._require_status(OrderStatus.CONFIRMED) # INVARIANT 5: Tracking required if not tracking_number or not tracking_number.strip(): raise OrderException("Tracking number required for shipment") self._tracking_number = tracking_number self._status = OrderStatus.SHIPPED def cancel(self, reason: str) -> None: # INVARIANT 3 (modified for cancel): Can cancel from PENDING or CONFIRMED if self._status not in (OrderStatus.PENDING, OrderStatus.CONFIRMED): raise OrderException(f"Cannot cancel order in {self._status} state") self._status = OrderStatus.CANCELLED self._cancellation_reason = reason def _require_status(self, required: OrderStatus) -> None: """Guard method for state transitions.""" if self._status == OrderStatus.CANCELLED: # INVARIANT 4: Cancelled is terminal raise OrderException("Cancelled orders cannot be modified") if self._status != required: raise OrderException( f"Expected {required.value}, but order is {self._status.value}" ) # Invariant verification checklist INVARIANT_CHECKLIST = """For each invariant, verify: ☐ Where is it enforced? (constructor, method guard, property)☐ Are there any bypass paths? (direct field access, reflection, setters)☐ What happens if enforcement fails? (exception, silent corruption)☐ Is enforcement tested? (unit tests specifically for invariants)☐ Is the invariant documented? (docstring, comments) For aggregate invariants across multiple objects: ☐ Which object is responsible for enforcement? (aggregate root)☐ Can related objects be modified outside the aggregate? (encapsulation leak)☐ Are database constraints a backup enforcement? (belt AND suspenders)"""The invariant validation process:
ORMs can violate invariants by hydrating objects without calling constructors. Serialization libraries can set private fields. Reflection bypasses all guards. Defense in depth: have database constraints as backup, and test invariants with integration tests that include real persistence.
Edge cases are where designs most often fail. Systematic exploration of boundary conditions and unusual scenarios reveals gaps that happy-path thinking misses.
Edge case categories:
1234567891011121314151617181920212223242526272829303132333435363738
# Edge Case Analysis: Inventory Management ## Entity: Product (with inventory_count) ### Boundary Values | Case | Question | Design Answer ||------|----------|---------------|| inventory_count = 0 | Can product be viewed? Listed? Added to cart? | Viewable + listed, cannot add to cart || inventory_count < 0 | Can this state exist? | NO - decrement check in Order.confirm() || Very large count | Any max limit? Integer overflow? | Use int64, no practical limit | ### Timing Scenarios | Case | Question | Design Answer ||------|----------|---------------|| Two orders for last item | Who wins? Loser's experience? | First to confirm() wins, second gets InventoryException || Restock during order | If restocked after "out of stock" error? | Customer must retry. Consider: queue backorders? || Inventory update during checkout | Cart shows "3 available" but only 1 when confirm | confirm() re-checks; customer gets partial fill or error | ### Failure Scenarios | Case | Question | Design Answer ||------|----------|---------------|| Decrement succeeds, order persist fails | Inventory forever decremented? | ⚠️ GAP - need transaction or compensation || Partial order fulfillment | 3 items ordered, only 2 available after race | Options: (1) reject all, (2) partial fill. Need decision. || Duplicate decrement (retry) | If confirm() called twice due to network timeout | Idempotency: check order already confirmed? ⚠️ GAP | ### Decisions Required 1. **Partial fulfillment policy**: Reject entire order or allow partial? - Recommendation: Offer customer choice, need UI design 2. **Backorder support**: Queue orders when OOS and fulfill later? - Recommendation: V2 feature, design BackorderQueue entity 3. **Idempotency for retries**: How to make confirm() safe to retry? - Recommendation: Add confirmation_token, check before processingEvery edge case you document should become a test case. This serves two purposes: (1) ensures the design actually handles the case, and (2) prevents regression if someone changes the code later. The edge case documentation becomes your test plan.
A comprehensive design review checklist synthesizes all validation techniques into a systematic process you can apply to any domain model.
The most effective design reviews involve developers, domain experts, and product stakeholders. Developers catch technical issues; domain experts catch model inaccuracies; product managers catch missing requirements. Multiple perspectives together find far more issues than any single reviewer.
Validation is not about proving your design is perfect—it's about finding imperfections cheaply, before they become expensive to fix. The techniques in this page form a safety net that catches gaps before coding begins.
Module Complete:
You've now learned the complete journey from real-world concepts to validated code structures:
With these skills, you can create domain models that accurately represent business reality, remain maintainable as requirements evolve, and form a solid foundation for the system you build.
You've completed the module on Real-World to Code Translation. You now have systematic techniques for the critical skill of translating business domains into clean, correct code structures. This foundation will serve you throughout your career in software design and architecture.