Inventory Management
DDD-inspired inventory system with Specification for composable product queries and reorder rules, Repository for clean data access, and Strategy for configurable replenishment algorithms.
Key Abstractions
Interface : is_satisfied_by(item) with and_spec/or_spec/not_spec combinators for composable queries
Concrete specifications for filtering products by availability, category, price range, and reorder eligibility
Repository : add, find_by_id, find_matching(spec), find_all. Collection-like abstraction over data access
Entity : id, name, category, price, quantity, reorder_point. Core domain object
Strategy : JustInTime orders exactly what's needed, SafetyStock adds a buffer, EconomicOrderQuantity optimizes batch size
State enum : RECEIVED, PICKING, PACKING, SHIPPED, DELIVERED. Tracks warehouse order lifecycle
Observer : alerts on low stock events and triggers automatic reorder workflows
Class Diagram
How It Works
Two patterns from Domain-Driven Design fit together naturally here: Specification and Repository. Together, they solve the eternal problem of "how do I query my domain objects without scattering filter logic everywhere?"
The Specification Pattern encodes a business rule as an object. InStockSpec knows what "in stock" means. CategorySpec("electronics") knows what "belongs to electronics" means. The key is the combinator methods: and_spec, or_spec, and not_spec let you compose simple specs into complex queries without writing new classes. InStockSpec().and_spec(CategorySpec("electronics")) reads almost like English.
The Repository Pattern presents data access as a collection. You add products, find_by_id, or find_matching(spec). The repository accepts a specification object and returns all matching items. This pair shows up constantly in DDD for good reason: the repository does not need a new method for every query shape. One find_matching method handles infinite query combinations.
The same Specification pattern serves two roles in this system. First, it drives product search and filtering (what the user sees). Second, it identifies products that need reordering (BelowReorderPointSpec). Same interface, same composition rules, completely different business context. That dual use is exactly why the pattern exists: it externalizes rules so you can reuse them across features.
Strategy handles the replenishment decision. Different product categories have different inventory economics. Perishables rot, so JustInTime orders exactly the deficit. Electronics have long lead times, so SafetyStock maintains a buffer. Furniture ships in bulk, so EconomicOrderQuantity uses fixed batch sizes. The strategy is assigned per category, not per product, keeping configuration manageable.
Observer closes the loop. When stock drops below the reorder point, observers fire alerts. The inventory service does not know what happens with those alerts: it just notifies. One observer logs to console. Another could send a Slack message. Another could auto-trigger a purchase order.
Requirements
Functional
- Product catalog with categories, prices, and stock levels
- Composable product queries: filter by category, price range, stock status, and combinations thereof
- Automatic reorder detection when stock falls below reorder points
- Per-category replenishment strategies with different order quantity calculations
- Warehouse order lifecycle tracking (received through delivered)
- Low-stock alerts and reorder notifications
Non-Functional
- Query logic must be reusable across features (search, reporting, reorder rules)
- Data access layer must be swappable (in-memory for tests, database for production)
- Replenishment algorithms must be configurable per product category
- Alert system must be extensible without modifying inventory logic
Design Decisions
What if we just added more repository methods instead?
The alternative is to add a method for every query shape: findByCategory, findByPriceRange, findByCategoryAndPriceRange, findInStockByCategory. This leads to a combinatorial explosion. With six filter dimensions, you need dozens of methods. Specification gives you six classes and infinite combinations via and_spec/or_spec/not_spec. Adding a new filter dimension means one new class, zero changes to the repository.
Is the in-memory repository just for testing?
The InMemoryProductRepository is not just a shortcut for testing. It proves that business logic is decoupled from storage. The InventoryService calls repo.find_matching(spec) without knowing whether products live in a HashMap, PostgreSQL, or an external API. Swapping the repository implementation is a constructor change, not a rewrite.
Can't we use one reorder formula for everything?
Hardcoding reorder logic means a single algorithm for all products. But perishable goods with a 3-day shelf life cannot use the same reorder math as electronics with a 6-week lead time. Strategy lets you assign the right algorithm per category and change it at runtime. Seasonal products might even switch strategies as demand shifts.
Should the inventory service send Slack messages directly?
The inventory service should not know about Slack, email, or dashboard updates. It knows stock changed. Observers decide what to do about it. This is especially important because alert channels change frequently. Adding SMS notifications should not require touching inventory code.
Interview Follow-ups
- "How would you implement CQRS for read-heavy inventory?" Separate the read model (optimized for search queries, denormalized, possibly cached) from the write model (handles stock updates with full validation). The Specification pattern maps cleanly to the read side: specs become query predicates against a read-optimized store.
- "How would you add event sourcing for stock movements?" Replace direct quantity mutation with a
StockEventlog (received, sold, damaged, transferred). Current quantity is computed by replaying events. This gives you a full audit trail and the ability to reconstruct inventory state at any point in time. - "How would you handle distributed inventory across warehouses?" Each warehouse gets its own repository. A
FederatedRepositoryaggregates queries across warehouses. Specifications remain unchanged: they operate on products regardless of which warehouse holds them. Replenishment strategies become warehouse-aware, considering transfer costs between locations. - "How would you handle reserved stock for pending orders?" Add
reserved_quantityto Product. Available stock isquantity - reserved_quantity. TheInStockSpecchecks available stock, not raw quantity. Reservations expire after a timeout to prevent phantom locks on inventory.
45-Minute LLD Playbook
Each phase is what the interviewer expects you to do and say. Concrete steps, not topic hints. Diagrams are what you would sketch on the board.
- 15 min
Clarify the inventory domain and lock the query contract
GoalPin down the Product shape, the query types in scope, the reorder model, and the warehouse order lifecycle. End with the diagram-vs-code question.
Do & Say- ASK·1Open with: Product has id, name, category, price, quantity, reorder_point. Queries are by category, price range, in-stock, and below-reorder-point, composable with AND, OR, NOT. Reasonable shape? Lock the product fields and the four base specifications.
- SAY·2Pin reorder detection: A separate check_reorder_needs pass iterates products matching BelowReorderPointSpec and asks the per-category ReplenishmentStrategy how much to order. Perishables use JustInTime, electronics use SafetyStock with a multiplier, furniture uses fixed-batch EOQ. Park ABC classification, demand forecasting, and supplier SLAs as v2.
- SAY·3Confirm the warehouse order lifecycle: RECEIVED -> PICKING -> PACKING -> SHIPPED -> DELIVERED, no skipping. advance throws on illegal transitions. State pattern.
- SAY·4Pin observer scope: On low stock (quantity drops at or below reorder_point during update_stock), fire on_low_stock. On reorder placed (inside check_reorder_needs), fire on_reorder_triggered. Two events, no more. Slack notifications and Jira tickets plug in as new StockObserver subclasses.
- ASK·5Ask the process question: Class diagram first, or jump into code with the Specification combinators, the Repository interface, the per-category Strategy map?. Either way I will budget the 40 minutes deliberately.
Interviewer is grading: You name the two-use-of-Specification angle (search and reorder) unprompted. You commit to per-category strategies rather than one global formula. You park ABC classification and forecasting as v2 instead of letting them creep in.
- 25-10 min
Sketch the spec combinators and the service wiring
GoalLock the Specification base with and_spec / or_spec / not_spec combinators, name the four concrete specs, and write the InventoryService method signatures.
Do & Say- DRAW·1Sketch Specification: abstract is_satisfied_by(item), three default-method combinators and_spec / or_spec / not_spec that wrap self into AndSpec / OrSpec / NotSpec. Say: The combinators live on the base interface so every spec composes. Adding a new spec is one class, the combinators come for free.
- SAY·2Name the four concrete specs: InStockSpec (quantity > 0), CategorySpec(category), PriceRangeSpec(min, max), BelowReorderPointSpec (quantity at or below reorder_point). Write the example: CategorySpec(electronics).and_spec(PriceRangeSpec(0, 500)).
- DRAW·3Sketch ProductRepository: add, find_by_id, find_matching(spec), find_all. InMemoryProductRepository holds a dict and runs find_matching as a list comprehension. Say: One method handles every query shape; adding a new spec is zero changes to the repository.
- DRAW·4Sketch InventoryService signatures: add_product(product), search(spec), update_stock(product_id, delta), check_reorder_needs() returning a list of WarehouseOrder, set_strategy(category, strategy), add_observer(observer). Say: The service owns the strategies map and the observer list. The repository is constructor-injected so swapping in a database is a one-line change.
- SAY·5If a diagram is requested, draw three clusters: Specification at the top with four concretes and three combinators, ProductRepository in the middle with InMemoryProductRepository, InventoryService at the bottom wiring everything plus StockObserver and ReplenishmentStrategy. Otherwise verbalise the same relationships in 90 seconds.
Interviewer is grading: You commit to and_spec / or_spec / not_spec as default methods on the base interface, not as standalone helpers. You name find_matching(spec) as the single repository method that handles every query. You distinguish ProductRepository from InventoryService cleanly.
- 325 min
Code in this sequence (bottom-up)
GoalType the Specification hierarchy first, then concrete specs, then Product, then Repository, then strategies, then OrderState/WarehouseOrder, then observers, then InventoryService. Match the existing pythonCode order exactly.
Do & Say- SAY·1Start with Specification base. Abstract is_satisfied_by. Default methods and_spec, or_spec, not_spec wrap self into AndSpec/OrSpec/NotSpec. Code AndSpec (left AND right), OrSpec, NotSpec. Say: Combinators return new specs, so CategorySpec(x).and_spec(PriceRangeSpec(0, 500)) is a fresh AndSpec, not a mutation. (~5 min)
- SAY·2Code the four concrete specs: InStockSpec checks quantity > 0. CategorySpec takes a category in the constructor. PriceRangeSpec takes min_price and max_price. BelowReorderPointSpec checks quantity at or below reorder_point. Each is six lines. (~3 min)
- SAY·3Code Product as a dataclass: id, name, category, price, quantity, reorder_point. Static factory create() that generates a uuid prefix id. (~2 min)
- SAY·4Code ProductRepository abstract with add, find_by_id, find_matching(spec), find_all. Then InMemoryProductRepository: dict of id to Product, find_matching is [p for p in self._products.values() if spec.is_satisfied_by(p)]. Say: The repository never inspects the spec internals, it just calls is_satisfied_by. That is the whole point: the repository is decoupled from query shape. (~3 min)
- SAY·5Code ReplenishmentStrategy abstract (calculate_order_quantity). JustInTime: reorder_point - quantity. SafetyStock(multiplier): reorder_point * multiplier - quantity. EOQ: batch_size if below reorder, else 0. Say: Three economic models: perishables hold no buffer, electronics need lead-time buffer, furniture optimises ordering cost. (~4 min)
- SAY·6Code OrderState enum with five values. next_state() consults a dict and raises on terminal state (DELIVERED has no next). Code WarehouseOrder dataclass: id, product_id, quantity, state defaults to RECEIVED. advance() captures old, calls state.next_state, prints the transition. (~3 min)
- SAY·7Code StockObserver abstract with on_low_stock(product) and on_reorder_triggered(product, order_qty). Then AlertingObserver as the concrete that prints both events. (~1 min)
- SAY·8Code InventoryService last. Holds repository, observers, strategies-by-category. add_product, search delegate. update_stock adjusts quantity and fires on_low_stock if at/below reorder. check_reorder_needs uses BelowReorderPointSpec, picks per-category strategy, fires on_reorder_triggered, builds WarehouseOrders. Say: Second use of Specification: same interface, different business context. (~4 min)
- SAY·9Self-test: Laptop (qty 15, reorder 10), phone (qty 3, reorder 10). SafetyStock(2.0) on electronics. search returns phone only (price 499.99). check_reorder_needs matches phone: target 20, deficit 17. WarehouseOrder for 17 phones. Observer fires. advance moves RECEIVED -> PICKING. (~1 min)
Interviewer is grading: Code compiles as you type. You name the second use of Specification (reorder detection) explicitly when coding check_reorder_needs. You volunteer that the repository never inspects spec internals. You walk through the laptop-phone example as a self-check before saying you are done.
- 45 min
Trade-offs and extensions
GoalDefend Specification over a method-per-query repository, defend per-category Strategy over a global formula, volunteer CQRS and event sourcing, close with one sentence.
Do & Say- SAY·1Trade-off one, Specification over a method-per-query repository: find_by_category, find_by_price_range, find_by_category_and_price_range, find_in_stock_by_category. With six filter dimensions you end up with dozens of methods, which is the combinatorial-explosion failure mode. Specification gives you six classes plus three combinators and infinite combinations. Adding a NEW filter is one new class, zero repository changes.
- SAY·2Trade-off two, per-category Strategy over a global formula: A three-day-shelf-life perishable cannot share reorder math with six-week-lead-time electronics. One formula either over-orders perishables or under-orders electronics. Strategy assigns the right algorithm per category, swappable at runtime as supplier terms shift.
- SAY·3Extension to volunteer, CQRS for read-heavy inventory: Split the read model (search-optimised, denormalised, possibly cached) from the write model (stock updates with full validation). Specifications become query predicates against the read store. Writes flow through the InventoryService as today.
- SAY·4Extension to volunteer, event sourcing for stock movements: Replace direct quantity mutation with a StockEvent log: received, sold, damaged, transferred. Current quantity is the sum, current state is derivable. Full audit trail. The reorder check still uses BelowReorderPointSpec against the projected product state.
- WATCH·5Be ready for distributed inventory: Each warehouse gets its own ProductRepository. A FederatedRepository aggregates queries across warehouses. Specifications operate on Product regardless of which warehouse holds it. Strategies become warehouse-aware to account for transfer costs.
- SAY·6Close with one sentence: Specification for composable queries used in both search and reorder detection. Repository for storage-agnostic data access. Strategy for per-category replenishment. State for the warehouse order lifecycle. Observer for low-stock and reorder events. Five patterns, one InventoryService that wires them.
Interviewer is grading: You name the combinatorial explosion as the failure mode of method-per-query. You defend per-category Strategy with concrete examples (perishables vs electronics lead times). You volunteer CQRS and event sourcing as the natural scale-up moves. You can summarise the five patterns in one breath.
Code Implementation
from abc import ABC, abstractmethod
from enum import Enum
from dataclasses import dataclass, field
from typing import Optional
import uuid
# ── Specification Pattern ─────────────────────────────────────
class Specification(ABC):
"""Base specification interface with composable combinators."""
@abstractmethod
def is_satisfied_by(self, item) -> bool:
pass
def and_spec(self, other: "Specification") -> "Specification":
return AndSpec(self, other)
def or_spec(self, other: "Specification") -> "Specification":
return OrSpec(self, other)
def not_spec(self) -> "Specification":
return NotSpec(self)
class AndSpec(Specification):
def __init__(self, left: Specification, right: Specification):
self._left = left
self._right = right
def is_satisfied_by(self, item) -> bool:
return self._left.is_satisfied_by(item) and self._right.is_satisfied_by(item)
class OrSpec(Specification):
def __init__(self, left: Specification, right: Specification):
self._left = left
self._right = right
def is_satisfied_by(self, item) -> bool:
return self._left.is_satisfied_by(item) or self._right.is_satisfied_by(item)
class NotSpec(Specification):
def __init__(self, spec: Specification):
self._spec = spec
def is_satisfied_by(self, item) -> bool:
return not self._spec.is_satisfied_by(item)
# ── Concrete Specifications ───────────────────────────────────
class InStockSpec(Specification):
def is_satisfied_by(self, item) -> bool:
return item.quantity > 0
class CategorySpec(Specification):
def __init__(self, category: str):
self._category = category
def is_satisfied_by(self, item) -> bool:
return item.category == self._category
class PriceRangeSpec(Specification):
def __init__(self, min_price: float, max_price: float):
self._min_price = min_price
self._max_price = max_price
def is_satisfied_by(self, item) -> bool:
return self._min_price <= item.price <= self._max_price
class BelowReorderPointSpec(Specification):
def is_satisfied_by(self, item) -> bool:
return item.quantity <= item.reorder_point
# ── Product Entity ────────────────────────────────────────────
@dataclass
class Product:
id: str
name: str
category: str
price: float
quantity: int
reorder_point: int
@staticmethod
def create(name: str, category: str, price: float, quantity: int, reorder_point: int) -> "Product":
return Product(
id=str(uuid.uuid4())[:8],
name=name,
category=category,
price=price,
quantity=quantity,
reorder_point=reorder_point,
)
# ── Repository Pattern ────────────────────────────────────────
class ProductRepository(ABC):
"""Collection-like abstraction over product data access."""
@abstractmethod
def add(self, product: Product) -> None:
pass
@abstractmethod
def find_by_id(self, product_id: str) -> Optional[Product]:
pass
@abstractmethod
def find_matching(self, spec: Specification) -> list[Product]:
pass
@abstractmethod
def find_all(self) -> list[Product]:
pass
class InMemoryProductRepository(ProductRepository):
def __init__(self):
self._products: dict[str, Product] = {}
def add(self, product: Product) -> None:
self._products[product.id] = product
def find_by_id(self, product_id: str) -> Optional[Product]:
return self._products.get(product_id)
def find_matching(self, spec: Specification) -> list[Product]:
return [p for p in self._products.values() if spec.is_satisfied_by(p)]
def find_all(self) -> list[Product]:
return list(self._products.values())
# ── Strategy Pattern (Replenishment) ──────────────────────────
class ReplenishmentStrategy(ABC):
@abstractmethod
def calculate_order_quantity(self, product: Product) -> int:
pass
class JustInTimeStrategy(ReplenishmentStrategy):
"""Order exactly the deficit to reach the reorder point."""
def calculate_order_quantity(self, product: Product) -> int:
if product.quantity >= product.reorder_point:
return 0
return product.reorder_point - product.quantity
class SafetyStockStrategy(ReplenishmentStrategy):
"""Order enough to reach reorder_point * multiplier for a safety buffer."""
def __init__(self, safety_multiplier: float = 2.0):
self._multiplier = safety_multiplier
def calculate_order_quantity(self, product: Product) -> int:
target = int(product.reorder_point * self._multiplier)
if product.quantity >= target:
return 0
return target - product.quantity
class EconomicOrderQuantityStrategy(ReplenishmentStrategy):
"""Fixed batch size optimized for ordering cost vs holding cost."""
def __init__(self, batch_size: int = 50):
self._batch_size = batch_size
def calculate_order_quantity(self, product: Product) -> int:
if product.quantity > product.reorder_point:
return 0
return self._batch_size
# ── State Pattern (Order Lifecycle) ───────────────────────────
class OrderState(Enum):
RECEIVED = "received"
PICKING = "picking"
PACKING = "packing"
SHIPPED = "shipped"
DELIVERED = "delivered"
def next_state(self) -> "OrderState":
transitions = {
OrderState.RECEIVED: OrderState.PICKING,
OrderState.PICKING: OrderState.PACKING,
OrderState.PACKING: OrderState.SHIPPED,
OrderState.SHIPPED: OrderState.DELIVERED,
}
nxt = transitions.get(self)
if nxt is None:
raise ValueError(f"No transition from {self.value}")
return nxt
@dataclass
class WarehouseOrder:
id: str
product_id: str
quantity: int
state: OrderState = OrderState.RECEIVED
def advance(self) -> None:
old = self.state
self.state = self.state.next_state()
print(f" Order {self.id}: {old.value} -> {self.state.value}")
# ── Observer Pattern ──────────────────────────────────────────
class StockObserver(ABC):
@abstractmethod
def on_low_stock(self, product: Product) -> None:
pass
@abstractmethod
def on_reorder_triggered(self, product: Product, order_qty: int) -> None:
pass
class AlertingObserver(StockObserver):
def on_low_stock(self, product: Product) -> None:
print(f" [ALERT] Low stock: {product.name} has {product.quantity} units (reorder point: {product.reorder_point})")
def on_reorder_triggered(self, product: Product, order_qty: int) -> None:
print(f" [REORDER] Placing order for {order_qty} units of {product.name}")
# ── Inventory Service (ties everything together) ──────────────
class InventoryService:
def __init__(self, repository: ProductRepository):
self._repo = repository
self._observers: list[StockObserver] = []
self._strategies: dict[str, ReplenishmentStrategy] = {}
def add_observer(self, observer: StockObserver) -> None:
self._observers.append(observer)
def set_strategy(self, category: str, strategy: ReplenishmentStrategy) -> None:
self._strategies[category] = strategy
def add_product(self, product: Product) -> None:
self._repo.add(product)
def search(self, spec: Specification) -> list[Product]:
return self._repo.find_matching(spec)
def update_stock(self, product_id: str, delta: int) -> None:
product = self._repo.find_by_id(product_id)
if product is None:
raise ValueError(f"Product {product_id} not found")
product.quantity += delta
if product.quantity <= product.reorder_point:
for obs in self._observers:
obs.on_low_stock(product)
def check_reorder_needs(self) -> list[WarehouseOrder]:
reorder_spec = BelowReorderPointSpec()
needing_reorder = self._repo.find_matching(reorder_spec)
orders: list[WarehouseOrder] = []
for product in needing_reorder:
strategy = self._strategies.get(product.category)
if strategy is None:
strategy = JustInTimeStrategy()
qty = strategy.calculate_order_quantity(product)
if qty > 0:
for obs in self._observers:
obs.on_reorder_triggered(product, qty)
order = WarehouseOrder(
id=str(uuid.uuid4())[:8],
product_id=product.id,
quantity=qty,
)
orders.append(order)
return orders
# ── Demo ──────────────────────────────────────────────────────
if __name__ == "__main__":
# Setup repository and service
repo = InMemoryProductRepository()
service = InventoryService(repo)
service.add_observer(AlertingObserver())
# Configure per-category replenishment strategies
service.set_strategy("electronics", SafetyStockStrategy(safety_multiplier=2.0))
service.set_strategy("perishables", JustInTimeStrategy())
service.set_strategy("furniture", EconomicOrderQuantityStrategy(batch_size=20))
# Add products
laptop = Product.create("Laptop", "electronics", 999.99, 15, 10)
phone = Product.create("Smartphone", "electronics", 499.99, 3, 10)
headphones = Product.create("Wireless Headphones", "electronics", 79.99, 50, 15)
milk = Product.create("Organic Milk", "perishables", 4.99, 8, 20)
bread = Product.create("Sourdough Bread", "perishables", 6.49, 2, 10)
desk = Product.create("Standing Desk", "furniture", 349.99, 5, 8)
chair = Product.create("Ergonomic Chair", "furniture", 249.99, 0, 5)
for p in [laptop, phone, headphones, milk, bread, desk, chair]:
service.add_product(p)
# ── Query 1: Electronics under $500 ──
print("=== Electronics under $500 ===")
spec = CategorySpec("electronics").and_spec(PriceRangeSpec(0, 500))
results = service.search(spec)
for p in results:
print(f" {p.name}: ${p.price:.2f} (qty: {p.quantity})")
# ── Query 2: In-stock items ──
print("\n=== In-stock items ===")
in_stock = service.search(InStockSpec())
for p in in_stock:
print(f" {p.name}: qty {p.quantity}")
# ── Query 3: Items needing reorder ──
print("\n=== Items below reorder point ===")
low_stock = service.search(BelowReorderPointSpec())
for p in low_stock:
print(f" {p.name}: qty {p.quantity}, reorder at {p.reorder_point}")
# ── Query 4: Composed NOT spec - out-of-stock items ──
print("\n=== Out-of-stock items ===")
out_of_stock = service.search(InStockSpec().not_spec())
for p in out_of_stock:
print(f" {p.name}: qty {p.quantity}")
# ── Reorder check with different strategies ──
print("\n=== Checking reorder needs (per-category strategies) ===")
orders = service.check_reorder_needs()
# ── Advance order states ──
print("\n=== Advancing warehouse orders ===")
for order in orders:
product = repo.find_by_id(order.product_id)
name = product.name if product else order.product_id
print(f" Order for {name} ({order.quantity} units):")
order.advance() # RECEIVED -> PICKING
order.advance() # PICKING -> PACKING
order.advance() # PACKING -> SHIPPED
# ── Simulate stock depletion triggering observer ──
print("\n=== Simulating stock depletion ===")
service.update_stock(laptop.id, -10)Interview Grading by Level
What an interviewer at each level expects to see in your answer. Use this to calibrate, not to perform.
Junior Engineer (L3)
Builds a product list with category and price filters, but writes one method per query and bakes reorder logic into the service.
- Implements Product with the right fields including reorder_point.
- Builds a basic repository with an in-memory dict.
- Recognises that reorder needs detection based on stock vs reorder point.
- Adds some kind of low-stock alert mechanism.
- Handles warehouse order states as an enum.
- Writes find_by_category, find_by_price_range, find_by_category_and_price_range as separate methods, hitting the combinatorial explosion before the interview ends.
- Buries reorder calculation as if/else inside InventoryService, no Strategy.
- Uses a single global reorder formula for all products, ignoring perishables vs electronics.
- Mixes query logic into the service layer instead of putting it on Specification.
- Mutates a single shared Specification instance, corrupting other features that hold a reference.
Mid-Level Engineer (L4)
Drives the design with composable Specification, a real Repository, per-category Strategy, and clean Observer fanout.
- Specification interface with and_spec / or_spec / not_spec combinators on the base.
- Four concrete specs (InStockSpec, CategorySpec, PriceRangeSpec, BelowReorderPointSpec) all immutable.
- ProductRepository.find_matching(spec) as the single query method; InMemoryProductRepository implements it as a filter.
- Three replenishment strategies (JustInTime, SafetyStock with multiplier, EOQ with batch size), assigned per category through set_strategy.
- OrderState enum with next_state map driving WarehouseOrder.advance.
- Reuses BelowReorderPointSpec inside check_reorder_needs, demonstrating the dual use of Specification.
- Does not volunteer CQRS or event sourcing for stock movements until prompted.
- Misses reserved quantity (quantity vs available quantity) as the natural extension for pending orders.
- Treats distributed inventory as out-of-scope without naming a FederatedRepository as the bridge.
Senior Engineer (L5+)
Volunteers CQRS, event sourcing, distributed inventory, and reserved-stock semantics, and frames Specification as the pattern that beats combinatorial explosion.
- Volunteers CQRS with the search-optimised read store driven by Specifications as query predicates and the write store handling stock updates.
- Volunteers event sourcing with a StockEvent log (received, sold, damaged, transferred) and a projected current-quantity view.
- Volunteers a FederatedRepository for distributed inventory across warehouses, with strategies becoming warehouse-aware for transfer costs.
- Volunteers reserved_quantity vs raw quantity, where InStockSpec checks available stock and reservations expire after a timeout.
- Names combinatorial explosion as the exact failure mode that Specification prevents.
- Frames each pattern around its failure mode: Repository for storage churn, Strategy for category-specific economics, State for illegal-jump prevention, Observer for alert-channel proliferation.
- Closes with a one-sentence summary that names all five patterns plus the dual use of Specification in under 20 seconds.
Common Mistakes
- ✗Embedding query logic in service classes: leads to duplicated filtering across different features
- ✗Not separating specification from repository: putting WHERE clause logic inside the repository method names (findByPriceAndCategory)
- ✗Mutable specifications: shared specs modified in one feature corrupt another
- ✗Single replenishment strategy for all products: perishables need JustInTime, electronics need SafetyStock
Key Points
- ✓Specification + Repository: `repo.find_matching(InStockSpec().and_spec(CategorySpec('electronics')))` accepts spec objects for querying. This pair shows up constantly in DDD for good reason.
- ✓Two uses of Specification in one system: (1) product search/filtering (2) reorder eligibility rules. Same pattern, different domains.
- ✓Repository: abstracts data access behind a collection-like interface. Swap InMemoryRepository for DatabaseRepository without changing business logic.
- ✓Strategy: replenishment algorithms calculate different reorder quantities. JustInTime orders exactly what's needed, SafetyStock adds a buffer.