
Part 1. Building an Architecture to Support Domain Modeling
Chapter 1. Domain Modeling
What is a Domain Model
- Business logic layer : central layer of 3 layered architecture
- domain : fancy way of saying the problem you're trying to solve
- model : map of a process of phenomenon that captures a useful property
- domain model : mental map that business owners have of their businesses
Exploring the Domain Lanaguage
- Understanding the domain model takes time and patience, and Post-it notes
- We have initial conversation with our business experts
Unit Testing Domain Models
def test_allocating_to_a_batch_reduces_the_available_quantity():
batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
line = OrderLine('order-ref', "SMALL-TABLE", 2)
batch.allocate(line)
assert batch.available_quantity == 18
@dataclass(frozen=True)
class OrderLine:
orderid: str
sku: str
qty: int
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self.available_quantity = qty
def allocate(self, line: OrderLine):
self.available_quantity -= line.qty
+) 느낀점 : models.py 안에 다 넣어놓네..? Flask 를 쓰긴 했지만, 이게 맞는 건가? model 을 따로 만들어야 하나..?
More Types For More Type hints
from dataclasses import dataclass
from typing import NewType
Quantity = NewType("Quantity", int)
Sku = NewType("Sku", str)
Reference = NewType("Reference", str)
...
class Batch:
def __init__(self, ref: Reference, sku: Sku, qty: Quantity):
self.sku = sku
self.reference = ref
self._purchased_quantity = qty
- wrapping primitive types by using typing.NewType
Dataclasses Are Great for Value Objects
Value Object
- business concept that has data but no identity
- any domain object that is uniquely identified by the data it holds
@dataclass(frozen=True)
class OrderLine:
orderid: OrderReference
sku: ProductReference
qty: Quantity
Value Objects and Entities
- entity : describe a domain object that has long-lived identity
def test_name_equality():
assert Name("Harry", "Percival") != Name("Barry", "Percival")
- People do change their names and their marital status etc, but we continue to recognize them as the same individual
class Person:
def __init__(self, name: Name):
self.name = name
- Entity unlike values have identity equality
- We can change their values and they are still recognizably the same thing
+) explicit in code by implementing equality operators on entites
class Batch:
...
def __eq__(self, other):
if not isinstance(other, Batch):
return False
return other.reference == self.reference
def __hash__(self):
return hash(self.reference)
Not Everything Has to Be and Object: A Domain Service Function
def test_prefers_current_stock_batches_to_shipments():
in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
line = OrderLine("oref", "RETRO-CLOCK", 10)
allocate(line, [in_stock_batch])
assert in_stock_batch.available_quantity == 90
def allocate(line: OrderLine, batches: List[Batch]) -> str:
batch = next(
b for b in sorted(batches) if b.can_allocate(line)
)
batch.allocate(line)
return batch.reference
Python's Magic Methods Let Us Use Our Models with Idiomatic Python
class Batch:
...
def __gt__(self, other):
if self.eta is None:
return False
if other.eta is None:
return True
return self.eta > other.eta
Exception Can Express Domain concepts Too
- Rasing a domain exception
class OutOfStock(Exception):
pass
def allocate(line: OrderLine, batches: List[Batch]) -> str:
try:
batch = next(
...
except StopIteration:
raise OutOfStock(f'Out of stock for sku {line.sku}')
Chapter 2. Repository Pattern
- Repository Pattern : simplifying abstraction over data storage, allowing us to de ouple our model layer from the data layer
Applying the DIP to Data Access


The "Normal" ORM Way: Model Depends on ORM
- object-relational mappers (ORMs) : exist to bridge the conceptual gap between the world of objects and domain modeling and the world of databases and relational algebra
- ORM gives us is persistence ignorance : fancy model doesn't need to know anything about how data is loaded or persisted
Inverting the Dependency: ORM Depends on Model
from sqlalchemy.orm import mapper, relationship
import model
metadata = MetaData()
order_lines = Table(
'order_lines', metadata,
Column('id', Integer, primary_key=True, autoincrement=True),
Column('sku', String(255)),
Column('qty', Integer, nullable=False),
Column('orderid', String(255)),
)
...
def start_mappers():
lines_mapper = mapper(model.OrderLine, order_lines)
def test_orderline_mapper_can_load_lines(session):
session.execute(
'INSERT INTO order_lines (orderid, sku, qty) VALUES ("order1", "RED-CHAIR", 12)'
)
expected = [
model.OrderLine("order1", "RED-CHAIR", 12)
]
assert session.query(model.OrderLine).all() == expected
- You probably wouldn't keep these tests around
- you've taken the step of inverting the dependency of ORM and domain model
- easier to write tests against and will provide a simple interface for faking out later in tests
Introducing the Repository Pattern
- Repository Pattern : abstraction over persistent storage
- it hides the boring details of data access by pretending that all of our data is in memory
- get your data from somewhere
import all_my_data
def create_a_batch():
batch = Batch(...)
all_my_data.batches.add(batch)
- Eventhough our objects are in memory, we need to put them somewhere so we can find them again
- Our memory data would let us add new objects, just like a list or a set
- the objects are in memory, we never need to call a
.save() method
- we just fetch the objects we care about and modify it in memory
The Repository in the Abstract
class AbstractRepository(abc.ABC):
@abc.abstractmethod
def add(self, batch: model.Batch):
raise NotImplementedError
@abc.abstractmethod
def get(self, reference) -> model.Batch:
raise NotImplementedError
What is the Trade-Off?
def test_repository_can_save_a_batch(session):
batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=None)
repo = repository.SqlAlchemyRepository(session)
repo.add(batch)
session.commit()
rows = list(session.execute(
'SELECT reference, sku, _purchased_quantity, eta FROM "batches"'))
assert rows == [("batch1", "RUSTY-SOAPDISH", 100, None)]
class SqlAlchemyRepository(AbstractRepository): d
ef __init__(self, session):
self.session = session
def add(self, batch):
self.session.add(batch)
def get(self, reference):
return self.session.query(model.Batch).filter_by(reference=reference).one()
def list(self):
return self.session.query(model.Batch).all()
@flask.route.gubbins
def allocate_endpoint():
batches = SqlAlchemyRepository.list()
lines = [
OrderLine(l['orderid'], l['sku'], l['qty']) for l in request.params...
]
allocate(lines, batches)
session.commit()
return 201
+) Chapter 2. Repository Pattern Excercise
Building a Fake Repository for Tests Is Now Trivial!
class FakeRepository(AbstractRepository):
def __init__(self, batches):
self._batches = set(batches)
def add(self, batch):
self._batches.add(batch)
def get(self, reference):
return next(b for b in self._batches if b.reference == reference)
def list(self):
return list(self._batches)

+) 진짜..? 흠..
Chapter 3. A Brief Interlude: On Coupling and Abstractions
- The Repository pattern is an abstraction over pemanent storage
What makes a good abstraction?
What do wwe want from abstractions?
- that we can use simple abstractions to hide messy details
- When we're unable to change component A for fear of breaking component B, we say that the components have become
coupled

- reduced the degree of coupling by inserting a new, simpler abstraction
Abstracting State Aids Testability
- 뭐 Abstracting 을 쓰면 당연히 Test 가 간단해지겠죠? 넘어가시죠
Mock (London School TDD) vs Fakes (Classic Style)
Mocks Aren't Stubs
- It also probably doesn't help that the MagicMock objects provided by unittest.ock aren't strictly speaking, mocks; they're spies
- But they're also often used as stubs or dummies
- There, we promise we're done with the test double terminology nitpicks now
+) 이렇게까지 말할 필요가 있어..? ㅠ 흑..
Chapter 4. Our First Use Case: Flask API and Service Layer
여기는 그냥 여러 Example 들이 있는 데 왜 난 보고 싶지 않지
Why is Everything Called a Service
domain service & service layer
application service (service layer)
- get some data from the database
- update the domain model
- persis any changes
domain service
- pice of logic that belongs in the domain model
- but doesn't sit naturally inside a stateful entity or value object
Chapter 5. TDD in High Gear and Low Gear
- service layer to capture some of the additional orchestration responsibilities we need from a working application
- service layer helps us clearly define our use cases and the workflow for each
- what we need to get from our repositories
- what pre-checks and current state validation we should do,
- and what we save at the end
Should Domain Layer Tests Move to the Service Layer?
- Tests are supposed to help us change our system fearlessly
- often we see teams writing too many tests against their domain model
- this causes problems when they come to change their codebase and find that they need to update tnes or even hundereds of unit tests
On Deciding What Kind of Tests to Write
- we can rewrite our entire application and so long as we don't change the URLs or request formats, our HTTP tests will continue to pass
- This gives us confidence that large-scale changes (ex) DB Schema)
- the tests we wrote in Chapter 1 helped us to flesh out our understnading of the objects we need
- the tests guided us to a design that makes sense and reads in the domain language
- tests are written in the domain language, they act as living documentation for our model
- we often sketch new behaviors by writing tests at this level to see how the code might look
we will need to replace or delete these tests, because they are tightly coupled to a particular implementation
High and Low Gear
- we prefer to write tests against services because of the lower coupling and higher coverage
Chapter 6. Unit of Work Pattern
- Unit of Work (UoW) Pattern : our abstraction over the idea of atomic operations
- allow us to finally and fully decouple our service layer from the data layer
def allocate(
orderid: str, sku: str, qty: int,
uow: unit_of_work.AbstractUnitOfWork
) -> str:
line = OrderLine(orderid, sku, qty)
with uow:
batches = uow.batches.list()
...
batchref = model.allocate(line, batches)
uow.commit()
- UoW as a context manager
- uow.batches : batches repo, so UoW provides us access to our permanent storage
Unit of Work and Its Context Manager
class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
def __init__(self, session_factory=DEFAULT_SESSION_FACTORY):
self.session_factory = session_factory
...
class FakeUnitOfWork(unit_of_work.AbstractUnitOfWork):
def __init__(self):
self.batches = FakeRepository([])
...
FakeUnitOfWork and FakeRepository are tightly coupled, just like the real UnitOfWork, Repository classes
def test_add_batch():
uow = FakeUnitOfWork()
services.add_batch("b1", "CRUNCHY-ARMCHAIR", 100, None, uow)
assert uow.batches.get("b1") is not None
assert uow.committed
Don't mock what you don't own
- forces us to build these simple abstractions over messy subsystems
Chapter 7. Aggregates and Consistency Boundaries
What is an Aggregate
- if we can't lock the whole database every time we want to allocate an order line, what should we do instead?
- we want to protect the invariants of our system but allow for the greatest degree of concurrency
Aggregate Pattern
- aggregate : a domain object that contains other domain objects and lets us treat the whole collection as a single unit
- the only way to modify the objects inside the aggregate is to load the whold thing, and to call methods on the aggregate itself
- good idea to nominate some entities to be the single entrypoint for modifying their related objects
- our aggregate has a root entity that encapsulates access to items
Choosing an Aggregate
- somewhat arbitrary, but it's important
- aggregate will be the boundary where we make sure every operation ends in a consistent state
One Aggregate = One Repository
- we need to apply the rule that they are the only entities that are publicly accessible to the outside world
- only repositories we are allowed should be repositories that return aggregates
Optimistic Concurrency with Version Numbers
1. Use Version

2. Select for Update
def get(self, sku):
return self.session.query(model.Product) \
.filter_by(sku=sku) \
.with_for_update() \
.first()