How to mock the domain center in an onion architecture in Python
Define a repository interface and an in-memory mock to test domain services without touching infrastructure.
Python code
57 linesfrom abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict, List, Optional
@dataclass
class Order:
id: int
customer: str
items: List[str]
total: float
class OrderRepository(ABC):
@abstractmethod
def find_by_id(self, order_id: int) -> Optional[Order]:
pass
@abstractmethod
def save(self, order: Order) -> None:
pass
class InMemoryOrderRepository(OrderRepository):
def __init__(self) -> None:
self._orders: Dict[int, Order] = {}
def find_by_id(self, order_id: int) -> Optional[Order]:
return self._orders.get(order_id)
def save(self, order: Order) -> None:
self._orders[order.id] = order
class OrderService:
def __init__(self, repo: OrderRepository) -> None:
self._repo = repo
def get_order_total(self, order_id: int) -> float:
order = self._repo.find_by_id(order_id)
if order is None:
raise ValueError(f"Order {order_id} not found")
# Domain logic: apply 10% discount for large orders
total = order.total
if len(order.items) >= 3:
total *= 0.9
return round(total, 2)
if __name__ == "__main__":
repo = InMemoryOrderRepository()
repo.save(Order(id=1, customer="Alice", items=["book", "pen", "notebook"], total=50.00))
repo.save(Order(id=2, customer="Bob", items=["laptop"], total=1200.00))
service = OrderService(repo)
print(f"Order 1 total after discount: ${service.get_order_total(1)}")
print(f"Order 2 total after discount: ${service.get_order_total(2)}")
Output
Order 1 total after discount: $45.0
Order 2 total after discount: $1200.0
How it works
The OrderRepository abstract base class defines the contract for data access, letting the OrderService depend on an abstraction rather than a concrete database. InMemoryOrderRepository provides an isolated mock implementation using a plain dictionary — perfect for unit tests and local runs. The OrderService applies domain logic (10% discount for orders with 3+ items) without any knowledge of where orders are stored. Because the service only knows about the interface, swapping the mock for a real SQL or API-backed repository requires no changes to the domain core.
Common mistakes
- Missing the `@abstractmethod` decorator on interface methods, which breaks abstraction enforcement.
- Using a concrete repository class directly in the service constructor instead of the abstract type.
- Forgetting to reset in-memory state between tests, causing cross-test leakage.
Variations
- Use `unittest.mock.Mock` in tests instead of a hand-written in-memory repository.
- Add a `delete` method to the repository interface for completeness.
Real-world use cases
- Unit testing a domain service with a lightweight in-memory repository instead of a test database.
- Prototyping an order workflow locally before wiring up a production database.
- Simulating repository behavior in performance tests without external infrastructure.
Sponsored
More from System design patterns
- Build a BFF (Backend for Frontend) Mock Aggregator in Python medium
- Builder pattern for mocking complex objects in Python easy
- Circuit Breaker Pattern in Python: Closed, Open, and Half-Open States medium
- Create a Data Helper Class in Python easy
- Domain Driven Design Aggregate Root Example in Python medium
- Facade Pattern in Python with Mock Simplification medium
Keep learning
Related tutorials and quizzes for this topic.