Quickstart
This quickstart gets a first command running with ArchitectureKit in a few minutes: you start EventSourcingDB with Docker, define a command, an event, and the state to decide on, and execute the command. It uses a temporary setup for development and testing – for everything else, see the links at the end.
Before You Begin#
You need Rust 1.88 or later, which ArchitectureKit requires, and Docker to run EventSourcingDB.
Compatibility
ArchitectureKit for Rust 1.0 runs with EventSourcingDB 1.1 and later.
Starting EventSourcingDB#
ArchitectureKit stores its events in EventSourcingDB. Start it with the following command. On the first start, Docker downloads the image:
docker run -it -p 3000:3000 \
thenativeweb/eventsourcingdb run \
--api-token=secret \
--data-directory-temporary \
--http-enabled \
--https-enabled=false
This starts EventSourcingDB on port 3000, with the API token secret, a temporary data directory that is cleaned up on shutdown, and unencrypted HTTP.
For Development and Testing Only
These settings simplify the setup but are not secure for production. For production-ready configurations, see Running EventSourcingDB.
Creating a Package#
In a second terminal, create a package with Cargo, and add ArchitectureKit, serde to write and read the data of events, and Tokio to run it all:
cargo new library
cd library
cargo add architecturekit
cargo add serde --features derive
cargo add tokio --features macros,rt-multi-thread
ArchitectureKit brings the client SDK for Rust along as architecturekit::eventsourcingdb, so that its version always fits the one of the kit, and you do not add it yourself.
Defining a Command, an Event, and the State#
Replace the content of src/main.rs with the following. A command describes what someone wants to do, an event describes what has happened, and the state holds what a command needs to decide on. The example acquires a book for a library:
use architecturekit::{
Command, Decider, DomainError, Event, Events, Precondition, State, Store, Subject,
eventsourcingdb::Client,
};
use serde::{Deserialize, Serialize};
#[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_state_read()]
}
}
#[derive(Serialize, Deserialize, Event)]
#[event(type = "io.eventsourcingdb.library.book-acquired")]
struct BookAcquired {
title: String,
author: String,
isbn: String,
}
#[derive(Events)]
enum BookEvent {
Acquired(BookAcquired),
}
#[derive(Clone, Default)]
struct Book {
is_acquired: bool,
}
impl State for Book {
type Event = BookEvent;
fn evolve(self, event: &BookEvent) -> Self {
match event {
BookEvent::Acquired(_) => Book { is_acquired: true },
}
}
}
The command names the subject it acts on, and its precondition on_state_read makes sure that its events are only written if nothing has been written to that subject since the state was read. The event names its event type, and the derive derives the JSON schema of its data from its fields. The enum BookEvent lists the events the state evolves by, with one variant per event type, and evolve takes the state one event further, starting from its Default. For details, see Defining Commands, Defining Events, and Defining State.
Deciding on the Command#
A decider connects the state with the decision made on it. Create it with Decider::new, from an asynchronous closure that receives the command and the current state, and returns the events to write, or an error to refuse the command. Add it to src/main.rs:
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,
})])
})
}
For details, see Making Decisions.
Executing the Command#
Finally, create a client and a store, register the schemas of the events, and execute the command. Add the main function to src/main.rs:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("http://localhost:3000".parse()?, "secret");
let store = Store::builder(client, "https://library.eventsourcingdb.io").build();
store.register_schemas(BookEvent::schemas()).await?;
let written = store
.execute(
&acquire_book(),
AcquireBook {
book: Subject::new("/books/42"),
title: "2001 – A Space Odyssey".to_owned(),
author: "Arthur C. Clarke".to_owned(),
isbn: "978-0756906788".to_owned(),
},
)
.await?;
for event in &written {
println!("{} {} {}", event.id(), event.ty(), event.subject());
}
Ok(())
}
Store::builder takes the client and the source to use for all events you write, and build returns a Store, which reads and writes the events of all commands (see Creating a Store). register_schemas registers the schemas of the events with the database, which from then on rejects events that do not match them. execute reads the events of the book's subject, evolves the state from them, calls the decider, and writes the events it returns.
Run the program:
cargo run
Cargo builds the program, which takes a while the first time, and runs it. After Cargo's own messages, the program prints the ID, the type, and the subject of the written event:
0 io.eventsourcingdb.library.book-acquired /books/42
Run it again. This time, the state shows that the book has already been acquired, so the decider refuses the command, nothing is written, and main ends with the error of the decider, wrapped in the variant Decider of the error of execute:
Error: Decider(DomainError { message: "/books/42 has already been acquired" })
Shutting Down#
Press Ctrl+C in the first terminal to shut down EventSourcingDB. Since the data directory is temporary, the event is gone afterwards.
Next Steps#
- First Steps with ArchitectureKit builds a small library step by step, up to a tested HTTP API.
- Using Preconditions shows how to prevent duplicates and guard against concurrent changes, even when two commands run at the same time.
- Projections and Defining Queries cover the reading side, from events to the answers of queries.
- Testing Deciders shows how to test the decider without a database.
- Setting Up an HTTP API exposes commands and queries over HTTP.
Further Resources#
- Repository: thenativeweb/architecturekit-rust
- Registry: crates.io, with the documentation of the crate on docs.rs
- License (MIT): License file
- Issue tracker: GitHub Issues