3.8 Programming Patterns
Repository Standards says where code goes. This page says how the code in those folders is shaped, so that a class written by one person reads like a class written by another. Three patterns carry it: the domain is modeled on the business (domain-driven design), every piece receives what it depends on instead of creating it (dependency injection), and every object protects its own state (encapsulation and immutability).
3.8.1 Domain-Driven Design¶
Domain-driven design means the code speaks the language of the business. A farmer, an insurer and a developer should recognize the same words in a conversation, in the data model and in the code: an Animal has Calving events, a SourceFeed delivers files, a Farm belongs to an Organization.
| Concept | What it is | Where it lives |
|---|---|---|
| Entity | Something with an identity that stays the same across its versions: an animal, a sync run. Immutable: a change produces a new version | domain/entity |
| Type | A value with no identity or lifecycle of its own: a value object (a weight with its unit, a time range), an enum, an identifier. Immutable and compared by value | domain/type |
| Aggregate | A group of entities and types changed together under one set of rules, reached through one root entity | The root entity in domain/entity |
| Repository | The abstract contract to load and save one kind of aggregate | domain/repository, implemented in data/repository |
| Use case | One operation of the application, which loads aggregates, applies their rules and saves them. One per file, named <operation>_usecase |
domain/usecase |
| Domain event | A fact the domain announces after a change, such as calving.registered |
Published through the outbox |
| Bounded context | The boundary inside which a word has one meaning: each system, and each feature of a large package | A system, or a feature folder |
Rules that follow:
- Names come from the business, as written in the data model. A concept is not renamed between the database, the code and the screen.
- Entities and types are immutable. Once built, an object never changes: a business action returns a new instance, checked by the same rules as construction, and the use case saves it. Nobody can see a half-updated object, and the same object can be shared between threads without locks.
- One transaction changes one aggregate. An aggregate refers to another by its identifier, never by holding it, so each can be loaded and saved on its own.
- Rules live in the entity, not around it. "A calving cannot precede the animal's birth" is checked by the entity itself, so no use case can forget it.
- Contexts meet through contracts. One system calls another's API or reacts to its events; it never reads the other's tables.
3.8.2 Dependency Injection¶
A piece of code that creates its own database connection, clock or HTTP client cannot be tested without them and cannot be reused with others. With dependency injection, a piece declares what it needs and receives it from outside, usually when it is constructed.
- Use cases depend on abstract contracts declared in the domain, never on concrete implementations. A contract is a Rust trait or a Python abstract class (
abc.ABCwith@abstractmethodon every method), and it holds no implementation: a class that forgets a method fails when it is created, not when the method is first called. - Dependencies are passed through the constructor. Optional settings may have defaults; required collaborators never do.
- One place starts everything: the package's
injectionfolder, called from the entry point. In a package with features, each feature builds its own pieces in its owninjection, and the package's one calls them all (see Growing a Package). Nowhere else in the code creates the concrete implementations. - No service locator and no global state. A piece never asks a global container for its dependencies; what it needs is visible in its constructor.
- In tests, the same constructor receives fakes: an in-memory repository, a fixed clock.
// domain/repository/animal_repository.rs
pub trait AnimalRepository {
fn find(&self, id: AnimalId) -> Result<Option<Animal>, DomainError>;
fn save(&self, animal: &Animal) -> Result<(), DomainError>;
}
// domain/usecase/register_calving_usecase.rs
/// Registers a calving on an animal and saves the animal's new version.
///
/// Called by the controller of `POST /animals/{id}/calvings`, for a member who may edit the herd.
pub struct RegisterCalvingUseCase<R: AnimalRepository, C: Clock> {
animals: R,
clock: C,
}
impl<R: AnimalRepository, C: Clock> RegisterCalvingUseCase<R, C> {
pub fn new(animals: R, clock: C) -> Self {
Self { animals, clock }
}
}
// injection/mod.rs: the only place that knows the concrete types
let register_calving = RegisterCalvingUseCase::new(PostgresAnimalRepository::new(pool), SystemClock);
# domain/repository/animal_repository.py
class AnimalRepository(ABC):
@abstractmethod
def find(self, animal_id: AnimalId) -> Animal | None: ...
@abstractmethod
def save(self, animal: Animal) -> None: ...
# domain/usecase/register_calving_usecase.py
class RegisterCalvingUseCase:
"""Registers a calving on an animal and saves the animal's new version.
Called by the controller of `POST /animals/{id}/calvings`, for a member who may edit the herd.
"""
def __init__(self, animals: AnimalRepository, clock: Clock) -> None:
self._animals = animals
self._clock = clock
# injection/container.py: the only place that knows the concrete types
register_calving = RegisterCalvingUseCase(PostgresAnimalRepository(pool), SystemClock())
3.8.3 Encapsulation and Immutability¶
An object is responsible for keeping its own state valid. If anyone can write a field directly, nobody can guarantee the rules that field must follow. We therefore hide every field and expose only methods:
- Keep fields internal: Rust uses
snake_casewithout a visibility modifier; Python uses_snake_caseas a non-public convention. - Reading goes through a getter:
name()for the Rust fieldname, anameproperty for the Python field_name. - Entities and types have no setters. The constructor sets and checks every field. A change is a method named after the business action, such as
renamed()orwith_calving(), that returns a new instance and leaves the original untouched. In Rust these methods takeselfor&self, never&mut self. - Setters exist only outside the domain, on objects meant to change, such as the state of a screen in
presentation/view. They check the new value too, and refuse an invalid one with an error.
#[derive(Clone)]
pub struct Animal {
id: AnimalId,
birth_date: NaiveDate,
name: String,
}
impl Animal {
pub fn new(id: AnimalId, birth_date: NaiveDate, name: String) -> Result<Self, DomainError> {
if name.trim().is_empty() {
return Err(DomainError::EmptyName);
}
Ok(Self { id, birth_date, name })
}
pub fn id(&self) -> AnimalId { self.id }
pub fn birth_date(&self) -> NaiveDate { self.birth_date }
pub fn name(&self) -> &str { &self.name }
/// The same animal under a new name, checked like a new one.
pub fn renamed(&self, name: String) -> Result<Self, DomainError> {
Self::new(self.id, self.birth_date, name)
}
}
// In a use case: load, derive the new version, save it.
let animal = animals.find(id)?.ok_or(DomainError::NotFound)?.renamed(name)?;
animals.save(&animal)?;
class Animal:
def __init__(self, animal_id: AnimalId, birth_date: date, name: str) -> None:
if not name.strip():
raise EmptyNameError()
self._id = animal_id
self._birth_date = birth_date
self._name = name
@property
def id(self) -> AnimalId:
return self._id
@property
def name(self) -> str:
return self._name
def renamed(self, name: str) -> "Animal":
"""The same animal under a new name, checked like a new one."""
return Animal(self._id, self._birth_date, name)
A property without a setter makes animal.name = "..." fail. Python cannot stop code from writing animal._name directly; the underscore says it must not, and review enforces it.