Skip to main content

Todo CLI in Rust 2. Immutable domain and typed errors by layer

·2 mins
Rafael Fernandez
Author
Rafael Fernandez
Mathematics, programming, and life stuff
Todo CLI in Rust without fluff - This article is part of a series.
Part 2: This Article

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:

  • id
  • title
  • status
  • created_at
  • modified_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_at stable,
  • updates modified_at only when transition is valid.

This prevents subtle audit bugs where timestamps change even when state does not.

Layered error taxonomy
#

By responsibility:

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.

Todo CLI in Rust without fluff - This article is part of a series.
Part 2: This Article