Skip to content

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.ABC with @abstractmethod on 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 injection folder, called from the entry point. In a package with features, each feature builds its own pieces in its own injection, 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_case without a visibility modifier; Python uses _snake_case as a non-public convention.
  • Reading goes through a getter: name() for the Rust field name, a name property 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() or with_calving(), that returns a new instance and leaves the original untouched. In Rust these methods take self or &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.