MCPcopy Index your code
hub / github.com/disintegrate-es/disintegrate

github.com/disintegrate-es/disintegrate @disintegrate-postgres-v4.0.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release disintegrate-postgres-v4.0.1 ↗ · + Follow
488 symbols 1,030 edges 63 files 107 documented · 22%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Disintegrate

Crates.io Docs Github Pages

Disintegrate is a Rust library for building event-sourced applications.

In an event-sourced system, instead of storing only the current state of your application, you store every meaningful change as an event and use that log of events to reconstruct state when needed.

Disintegrate lets you store events in a durable event store and use them to build the state of your application dynamically, loading only the events you need and applying them to reconstruct state on the fly.

Traditionally with event sourcing you use aggregates (a DDD concept that groups related domain entities into a consistency boundary and enforces rules through a single root) to model and enforce business rules. Compared to aggregates, Disintegrate lets you query all relevant events first and then build whatever state you need to make a decision, rather than loading a fixed aggregate state first. This makes it easier to adapt to evolving business requirements and to compose multiple pieces of state for a decision.

Why Disintegrate?

Disintegrate offers a fresh perspective on designing business applications by shifting away from relying solely on aggregates. Instead, it enables developers to construct business concepts directly from an event stream. This approach allows for decentralized and dynamic architectures that can evolve over time.

By leveraging the event stream as the foundation, Disintegrate empowers developers to build models that capture the essence of business events without the need for multiple versions of the same event within aggregates. This reduces duplication and complexity, leading to cleaner and more maintainable code.

Key Features

  • Event Stream Modeling: Disintegrate enables the construction of business concepts directly from an event stream, providing a more flexible and decentralized approach.

  • Support for Aggregates: While promoting a new approach, Disintegrate still supports traditional aggregates, allowing developers to transition gradually or utilize both methodologies in their applications.

  • Adaptability to Changing Business Rules: Disintegrate allows for easier evolution and adaptation to evolving business rules over time. By decoupling models from aggregates, developers gain the freedom to adjust and refine business concepts without heavy dependencies.

  • TDD: The library provides a TestHarness util to encourage the use of Test-Driven Development (TDD). To begin with your new application implementation, you should first write tests that describe your business logic. This way, you can ensure that your implementation satisfies your business invariants. TestHarness enables writing tests in given-when-then style. given represents past events, when is a decision, and then are the events emitted by the decision or an error:

    ```rust,ignore #[test] fn it_withdraws_an_amount() { disintegrate::TestHarness::given([ DomainEvent::AccountOpened { account_id: "some account".into(), }, DomainEvent::AmountDeposited { account_id: "some account".into(), amount: 10, }, ]) .when(WithdrawAmount::new("some account".into(), 10)) .then([DomainEvent::AmountWithdrawn { account_id: "some account".into(), amount: 10, }]); }

    #[test]
    fn it_should_not_withdraw_an_amount_when_the_balance_is_insufficient() {
        disintegrate::TestHarness::given([
            DomainEvent::AccountOpened {
                account_id: "some account".into(),
            },
            DomainEvent::AmountDeposited {
                account_id: "some account".into(),
                amount: 10,
            },
            DomainEvent::AmountWithdrawn {
                account_id: "some account".into(),
                amount: 26,
            },
        ])
        .when(WithdrawAmount::new("some account".into(), 5))
        .then_err(Error::InsufficientBalance);
    }
    

    ```

Usage

To add Disintegrate to your project, follow these steps:

  1. Add disintegrate and disintegrate-postgres as a dependencies in your Cargo.toml file:

    toml [dependencies] disintegrate = "4.0.0" disintegrate-postgres = "4.0.1"

    • Disintegrate provides several features that you can enable based on your project requirements. You can include them in your Cargo.toml file as follows:

    toml [dependencies] disintegrate = { version = "4.0.0", features = ["macros", "serde-prost"] } disintegrate-postgres = { version = "4.0.1", features = ["listener"] }

    • The macros feature enables the use of derive macros to simplify events implementations.

    • For events serialization and deserialization, Disintegrate supports different serialization formats through the Serde ecosystem. You can enable the desired format by including the corresponding feature:

      • To enable JSON serialization, use the serde-json feature: features = ["serde-json"].
      • To enable Avro serialization, use the serde-avro feature: features = ["serde-avro"].
      • To enable Prost serialization, use the serde-prost feature: features = ["serde-prost"].
      • To enable Protocol Buffers serialization, use the serde-protobuf feature: features = ["serde-protobuf"].
    • If you're using the PostgreSQL event store backend and want to use the listener mechanism, you can enable the listener feature: disintegrate-postgres = {version = "4.0.1", features = ["listener"]}.

  2. Define the list of events in your application. You can use the Event Storming technique to identify the events that occur in your system. Here's an example of defining events using Disintegrate:

    ```rust,ignore use disintegrate::Event; use serde::{Deserialize, Serialize};

    [derive(Debug, Clone, PartialEq, Eq, Event, Serialize, Deserialize)]

    [stream(UserEvent, [UserCreated])]

    [stream(CartEvent, [ItemAdded, ItemRemoved, ItemUpdated, CouponApplied])]

    [stream(CouponEvent, [CouponEmitted, CouponApplied])]

    pub enum DomainEvent { UserCreated { #[id] user_id: String, name: String, }, ItemAdded { #[id] user_id: String, #[id] item_id: String, quantity: u32, }, ItemRemoved { #[id] user_id: String, #[id] item_id: String, }, ItemUpdated { #[id] user_id: String, #[id] item_id: String, new_quantity: u32, }, CouponEmitted { #[id] coupon_id: String, quantity: u32, }, CouponApplied { #[id] coupon_id: String, #[id] user_id: String, }, } ```

    In this example, we define an enum DomainEvent using the #[derive(Event)] attribute. The enum represents various events that can occur in your application. The #[stream] attribute specifies the event streams, such as UserEvent and CartEvent, and their corresponding variants. This allows you to organize events into logical streams. The #[id] attribute on fields allows you to specify the domain identifiers of each event, which are used for filtering relevant events for a state query.

  3. Create a state query for constructing a view from events by deriving the StateQuery trait. To achieve this, define the event stream using the #[state_query] attribute and annotate fields containing identifiers with #[id]. The library uses the annotated IDs to filter events in the specified stream, retaining only those with corresponding IDs. The state must also implement the StateMutate trait, which defines how the data contained in the events is aggregated to construct the state:

    ```rust,ignore use crate::event::{CartEvent, CouponEvent, DomainEvent}; use disintegrate::StateQuery; use disintegrate::{Decision, StateMutate}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use thiserror::Error;

    [derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]

    pub struct Item { id: String, quantity: u32, }

    impl Item { fn new(id: String, quantity: u32) -> Self { Item { id, quantity } } }

    [derive(Default, StateQuery, Clone, Serialize, Deserialize)]

    [state_query(CartEvent)]

    pub struct Cart { #[id] user_id: String, items: HashSet, applied_coupon: Option, }

    impl Cart { pub fn new(user_id: &str) -> Self { Self { user_id: user_id.into(), ..Default::default() } } }

    impl StateMutate for Cart { fn mutate(&mut self, event: Self::Event) { match event { CartEvent::ItemAdded { item_id, quantity, .. } => { self.items.insert(Item::new(item_id, quantity)); } CartEvent::ItemRemoved { item_id, .. } => { self.items.retain(|item| item.id != *item_id); } CartEvent::ItemUpdated { item_id, new_quantity, .. } => { self.items.replace(Item::new(item_id, new_quantity)); } CartEvent::CouponApplied { coupon_id, .. } => { self.applied_coupon = Some(coupon_id); } } } } ```

    In this example, the Cart struct represents the state of a shopping cart and keeps track of the items added by users.

  4. Create a struct that implements the Decision trait. This struct represents a business decision and is responsible for validating inputs and generating a list of changes. The resulting changes will be stored by the DecisionMaker in the event store:

    ```rust,ignore

    [derive(Debug, Error)]

    pub enum CartError { // cart errors }

    pub struct AddItem { user_id: String, item_id: String, quantity: u32, }

    impl AddItem { pub fn new(user_id: String, item_id: String, quantity: u32) -> Self { Self { user_id, item_id, quantity, } } }

    /// Implement your business logic impl Decision for AddItem { type Event = CartEvent; type StateQuery = Cart; type Error = CartError;

    fn state_query(&self) -> Self::StateQuery {
        Cart::new(&self.user_id)
    }
    
    fn process(&self, _state: &Self::StateQuery) -> Result<Vec<Self::Event>, Self::Error> {
        // check your business constraints...
        Ok(vec![CartEvent::ItemAdded {
            user_id: self.user_id.clone(),
            item_id: self.item_id.to_string(),
            quantity: self.quantity,
        }])
    }
    

    } ```

    In the provided examples, decisions are used as commands that are executed against a state built from the event store. A Decision defines the StateQuery, which will be mutated using the events contained in the event store.

    In cases where no events are found in the event store, the default StateQuery is used as a starting point to make the decision. This scenario arises when the decision is taken for the first time, and there is no historical data available to build a StateQuery.

  5. Instantiate an event store, create the AddItem decision, and invoke make method on DecisionMaker:

    ```rust,ignore mod cart; mod event;

    use cart::AddItem; use event::DomainEvent;

    use anyhow::{Ok, Result}; use disintegrate::{serde::json::Json, NoSnapshot}; use disintegrate_postgres::PgEventStore; use sqlx::{postgres::PgConnectOptions, PgPool};

    [tokio::main]

    async fn main() -> Result<()> { dotenv::dotenv().unwrap();

    // Create a PostgreSQL poll
    let connect_options = PgConnectOptions::new();
    let pool = PgPool::connect_with(connect_options).await?;
    
    // Create a serde for serialize and deserialize events
    let serde = Json::<DomainEvent>::default();
    
    // Create a PostgreSQL event store
    let event_store = PgEventStore::new(pool, serde).await?;
    
    // Create a Postgres DecisionMaker
    let decision_maker = disintegrate_postgres::decision_maker(event_store, NoSnapshot);
    
    // Make the decision. This performs the business decision and persists the changes into the
    // event store
    decision_maker
        .make(AddItem::new("user-1".to_string(), "item-1".to_string(), 4))
        .await?;
    Ok(())
    

    } ```

The provided example already illustrates a fully functional event-sourced application. But, if your business logic extends across multiple aggregates, you can employ a multi-state query to gather all the required data from various states. Typically, ensuring invariants across multiple aggregates would necessitate a policy between them. With the multi-state, represented by a tuple of StateQuery, you can validate all the invariants in a single Decision.

```rust,ignore

[derive(Default, StateQuery, Clone, Seri

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Method 201
Function 147
Class 92
Enum 26
Interface 22

Languages

Rust99%
TypeScript1%

Modules by API surface

disintegrate-postgres/src/listener.rs32 symbols
examples/banking/src/domain.rs31 symbols
disintegrate/src/utils.rs22 symbols
disintegrate/src/identifier.rs21 symbols
disintegrate/src/stream_query.rs19 symbols
disintegrate/src/state.rs19 symbols
examples/courses/src/domain/course.rs18 symbols
disintegrate/src/state_store.rs18 symbols
examples/courses/src/domain/subscription.rs14 symbols
disintegrate-postgres/src/listener/tests.rs14 symbols
disintegrate-postgres/src/event_store/tests.rs14 symbols
disintegrate/src/testing.rs13 symbols

For agents

$ claude mcp add disintegrate \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page