Docs / ArchitectureKit / Rust / Fundamentals / Making Decisions

Making Decisions

A decider connects a state with the decision made on it.

To create one, call Decider::new with an asynchronous closure that receives the command and the current state, and returns the events to write:

fn acquire_book() -> Decider<AcquireBook, Book, DomainError> {
  Decider::new(|command: AcquireBook, book: Book| async move {
    if book.is_acquired {
      return Err(DomainError::new(format!("{} has already been acquired", command.book)));
    }

    Ok(vec![BookEvent::Acquired(BookAcquired {
      title: command.title,
      author: command.author,
      isbn: command.isbn,
    })])
  })
}

The types of the command, the state, and the error are taken from the closure and from what it returns, so a decider bound with let names none of them, while a function that returns one names them in its return type, as above. The events are variants of the enum of the state, so a decider can only return events that the state has a rule for. Decider::new returns a Decider, whose decide method decides on a command, given the state, with the closure you handed over.

The closure takes the command and the state by value rather than by reference, so that its future can be sent across threads, which a web server needs. It is asynchronous, so that it can load what else it needs to decide, such as the state of another subject (see Loading States).

To refuse a command, return an error. DomainError refuses it with a message, which DomainError::new takes, for example from format!, and which is exactly what its Display shows. It belongs to the category Category::Domain (see Handling Errors).

To let a caller or a test tell a refusal apart from the others, define an error type of your own, such as an enum with one variant per refusal. It implements Categorized, whose category method names Category::Domain, so that the kit tells the refusal apart from a failure, for example one while the decider loads what else it needs:

#[derive(Debug, PartialEq)]
enum BookRefused {
  AlreadyAcquired(Subject),
}

impl fmt::Display for BookRefused {
  fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    match self {
      Self::AlreadyAcquired(book) => write!(formatter, "{book} has already been acquired"),
    }
  }
}

impl std::error::Error for BookRefused {}

impl Categorized for BookRefused {
  fn category(&self) -> Option<Category> {
    Some(Category::Domain)
  }
}

fn acquire_book() -> Decider<AcquireBook, Book, BookRefused> {
  Decider::new(|command: AcquireBook, book: Book| async move {
    if book.is_acquired {
      return Err(BookRefused::AlreadyAcquired(command.book));
    }

    Ok(vec![BookEvent::Acquired(BookAcquired {
      title: command.title,
      author: command.author,
      isbn: command.isbn,
    })])
  })
}

A caller then matches the variant, as in ExecuteError::Decider(BookRefused::AlreadyAcquired(_)) (see Handling Errors), and a test compares the error with then_failed, which is why it derives PartialEq and Debug (see Expecting Rejections). Display and Error let it be logged and handed on with ?.

A decider may check several rules:

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

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

If there is nothing to do, return no events. A decider that never refuses names its error type all the same, here in the return type, or with Decider::<_, _, DomainError>::new for one bound with let:

fn return_book() -> Decider<ReturnBook, Book, DomainError> {
  Decider::new(|_: ReturnBook, book: Book| async move {
    if !book.is_borrowed {
      return Ok(Vec::new());
    }

    Ok(vec![BookEvent::Returned(BookReturned {})])
  })
}