Docs / ArchitectureKit / Rust / Guides / Sharing Code Between Commands

Sharing Code Between Commands

The commands of one aggregate usually act on the same kind of subject, guarded by the same preconditions, and their deciders check the same things first. Write these once instead of for every command.

Sharing Subjects and Preconditions#

Define the subject and the preconditions on a generic struct of their own, which holds the subject together with whatever is particular to a command, and implement Command once for it. Every command of the aggregate then consists of only its particular fields, and the generic struct around it makes it a Command:

#[derive(Clone)]
struct OnBook<T> {
  book: Subject,
  action: T,
}

impl<T> Command for OnBook<T> {
  fn subject(&self) -> Subject {
    self.book.clone()
  }

  fn preconditions(&self) -> Vec<Precondition> {
    vec![Precondition::on_state_read()]
  }
}

#[derive(Clone)]
struct AcquireBook {
  title: String,
  author: String,
  isbn: String,
}

#[derive(Clone)]
struct BorrowBook {
  reader_id: String,
  borrowed_until: String,
}

#[derive(Clone)]
struct ReturnBook;

A command then names its book through the generic struct, as in OnBook { book: BOOKS.build(&["42"]), action: BorrowBook { reader_id: "23".to_owned(), borrowed_until: "2026-10-24".to_owned() } }, and a decider for OnBook<BorrowBook> reads the book as command.book, and the rest as command.action.reader_id.

The command that creates a book can use the generic struct, too, since on_state_read requires a subject without any events to still be pristine (see Guarding Against Concurrent Changes). That is different if the generic struct checks the revision of the caller instead (see Checking the Revision of the Caller): a new book has no revision yet. Then the creating command is a command of its own, and prevents duplicates itself:

use architecturekit::Revision;

#[derive(Clone)]
struct OnBook<T> {
  book: Subject,
  expected_event_id: Revision,
  action: T,
}

impl<T> Command for OnBook<T> {
  fn subject(&self) -> Subject {
    self.book.clone()
  }

  fn preconditions(&self) -> Vec<Precondition> {
    vec![Precondition::on_event_id(self.subject(), self.expected_event_id)]
  }
}

#[derive(Clone)]
struct AcquireBook {
  book: Subject,
  title: String,
  author: String,
  isbn: String,
}

impl Command for AcquireBook {
  fn subject(&self) -> Subject {
    self.book.clone()
  }

  fn preconditions(&self) -> Vec<Precondition> {
    vec![Precondition::on_pristine_subject(self.subject())]
  }
}

Sharing Checks#

Most deciders of an aggregate first check that it exists. Write that check once, as a function that returns a domain error, and call it with ? at the beginning of every decider that needs it:

use architecturekit::{Decider, DomainError};

fn require_acquired(book: &Book, subject: &Subject) -> Result<(), DomainError> {
  if !book.is_acquired {
    return Err(DomainError::new(format!("{subject} does not exist")));
  }

  Ok(())
}

fn borrow_book() -> Decider<OnBook<BorrowBook>, Book, DomainError> {
  Decider::new(|command: OnBook<BorrowBook>, book: Book| async move {
    require_acquired(&book, &command.book)?;
    if book.is_borrowed {
      return Err(DomainError::new(format!("{} is already borrowed", command.book)));
    }

    Ok(vec![BookEvent::Borrowed(BookBorrowed {
      borrowed_by: command.action.reader_id,
      borrowed_until: command.action.borrowed_until,
    })])
  })
}

Note that only the state knows what it means for an aggregate to exist, which is why the kit does not check it. The precondition that requires an existing subject protects the write (see Requiring an Existing Subject), but it fails with a conflict, which is transient. A check in the decider answers a command on a book that does not exist with a domain error instead.