Welcome back.
Read this chapter in Spanish: ES
With boundaries in place, we tackled the core question: what makes a task valid, and how do we enforce that every single time.
Code references:
Modeling Task around invariants #
Task holds:
idtitlestatuscreated_atmodified_at
The real value is not the fields. It is the transition model.
Immutable transitions (self -> Result<Self>)
#
We use:
pub fn mark_done(self) -> DomainResult<Self>
pub fn mark_todo(self) -> DomainResult<Self>
pub fn edit_title(self, title: String) -> DomainResult<Self>Why this instead of &mut self
#
With &mut self, business rules often get scattered:
- one flow updates status,
- another flow updates timestamps,
- another forgets validation.
With immutable transitions, every state change goes through one validated gate.
Example of a composed rule #
In mark_as, one transition enforces all of this:
- rejects same-state transitions,
- keeps
created_atstable, - updates
modified_atonly when transition is valid.
This prevents subtle audit bugs where timestamps change even when state does not.
Layered error taxonomy #
By responsibility:
- Domain: DomainError
- Repository: RepoError
- Application: ApplicationError
- CLI: CliError
Why not one global error type #
If everything ends up as “something failed”, you lose both technical and business context.
Real case:
done <id>with a missing task is not I/O failure.- It is a business error (
TaskNotFound).
Typing that explicitly improves debugging and test precision.
Use cases as orchestrators, not rule owners #
Use cases follow a stable flow: find -> transition -> save.
Examples:
Use cases coordinate. Domain decides validity.
Closing #
This is the line between “code that runs” and “code that guarantees behavior”.
Next chapter: CLI design with clap, strict input contracts, and output for both humans and automation.