Repository Contracts and Aggregate Rehydration¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Object-Oriented Programming"]
section["Persistence Serialization Schema Evolution"]
page["Repository Contracts and Aggregate Rehydration"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
boundary["state the aggregate contract"] --> load["load stored representation"]
load --> rebuild["rehydrate through an honest boundary"]
rebuild --> reject["reject or repair invalid history"]
reject --> protect["keep callers away from storage trivia"]
Read the first diagram as a placement map: this page establishes the foundation for the rest of Module 06. Read the second diagram as the reasoning route: name the aggregate contract first, then decide how stored representation becomes that aggregate again without letting storage history leak into caller expectations.
Why this lesson matters¶
Repositories are often taught too loosely. Learners hear "a repository loads and saves things" and then build generic data-access helpers that hand rows, ORM objects, or partial fragments back to the application layer.
That is not a harmless style preference. It changes who appears to own the system:
- the aggregate stops looking authoritative
- storage shape becomes part of application vocabulary
- invariant enforcement becomes optional during load
This lesson tightens the boundary before the rest of the persistence module builds on it.
A repository is a domain-facing contract¶
The first question is not "which database are we using?" It is:
What should a caller be able to ask for without learning storage structure?
Strong repository methods sound like:
- load a workshop enrollment by workshop identifier
- save one modified enrollment aggregate
- confirm whether an authoritative aggregate exists
Weak repository methods sound like:
- fetch workshop rows
- read attendee join entries
- update waitlist relation table
- expose generic CRUD over persistence objects
The difference is not cosmetic. A strong repository preserves the aggregate as the unit of meaning. A weak repository teaches callers to assemble meaning themselves.
What the repository owns¶
At minimum, a repository owns two responsibilities:
- loading one authoritative aggregate honestly
- saving one authoritative aggregate honestly
It does not automatically own:
- query-model reporting
- projection reads
- cross-aggregate commit coordination
- outbound publication delivery
Those may live nearby, but they are not the same contract.
This matters because repository surfaces become vague buckets very quickly when every data-related concern is pushed through one abstraction.
Rehydration is part of the contract, not an implementation footnote¶
Loading persisted state is not just retrieval. It is re-entry into the domain.
That means the repository must answer:
- how stored representation becomes domain values again
- where invariant-enforcing construction happens
- what counts as malformed or unsupported history
- how callers are protected from storage-specific repair steps
If the repository returns raw pieces and expects callers to reassemble the aggregate, it has already failed at the most important part of the job.
Worked contrast: weak load path versus strong load path¶
Weak path:
- fetch rows
- hand them to service code
- set fields manually on an object
- hope the data still makes sense
Strong path:
- fetch the stored representation
- decode it through one mapper or codec
- call an invariant-preserving constructor or factory
- return only a valid aggregate or fail honestly
The strong path is not stricter because it is fancier. It is stricter because it keeps the repository from becoming a corruption tunnel.
Stored data does not outrank domain rules¶
This is the sentence to remember from the page:
stored data is evidence of what happened, not automatic permission to create any object.
If the repository finds data that cannot be turned back into a valid aggregate, the system should usually:
- fail loudly
- route through deliberate repair or migration
- or reject that representation as unsupported
What it should not do is quietly normalize the problem away inside load logic and return something that only resembles the real aggregate.
That pattern is how corruption becomes durable truth.
Keep authoritative loads separate from read models¶
Students often widen repositories because they also need:
- search screens
- dashboards
- reporting summaries
- audit tables
Those needs are real, but they are usually not aggregate loads.
One good test is:
If the returned shape is not the authoritative aggregate, why is this method still on the aggregate repository?
If the answer is only convenience, the boundary is likely too wide. Separate read-model or query services protect the repository from becoming a catch-all surface.
Design repository methods to preserve aggregate authority¶
In practice, strong repository design often means:
- load whole authoritative units
- save whole authoritative units
- keep mutation rules outside raw storage shapes
- avoid partial update APIs that bypass aggregate logic
This is not dogma about always loading every possible relation. It is about refusing to let persistence mechanics redefine where the important invariants live.
Review drill: ask four questions about every repository¶
For any repository surface, ask:
- does the method speak in aggregate language or storage language?
- who reconstructs the aggregate from stored representation?
- what happens if historical data is malformed or unsupported?
- what read concern is being smuggled in that belongs elsewhere?
If you cannot answer one of those quickly, the boundary is probably still too weak.
Common mistakes¶
- returning raw rows or ORM entities to application code
- forcing callers to reassemble aggregates manually
- hiding corruption through silent coercion
- mixing query-model reads into the aggregate repository
- using "generic repository" as a reason to avoid naming the real contract
All of these mistakes blur the same line: who actually owns the meaning of the loaded object.
Capstone connection¶
Use the capstone repository surface to ask:
- what exactly counts as the authoritative aggregate here?
- which storage details are still hidden from the caller?
- where would malformed history be detected today?
- what method should move out because it is really a query or report surface?
Those questions turn the repository into a reviewable design boundary instead of a persistence utility.
Exit check¶
Leave this lesson only when you can do all of these:
- explain why a repository is a domain-facing contract rather than generic data access
- describe the honest rehydration route for one aggregate
- identify one query that belongs outside the aggregate repository because it is really a read-model concern