ocoda Event Sourcing Library

repository·master·Indexed 42 days ago

https://github.com/ocoda/event-sourcing

A NestJS library providing building blocks for implementing Domain-Driven Design (DDD), CQRS, and Event Sourcing patterns. It includes support for custom Event Stores and Snapshot Stores, event serialization, and event publishing to internal or external systems.

Tokens
37.8K
Snippets
104
Records
157
Agent score
54%

What's inside ocoda event-sourcing

  1. Overview of Ocoda Event Sourcing

    master

    Ocoda Event Sourcing is a library for NestJS applications designed to implement advanced architectural patterns. It provides building blocks for Domain-Driven Design (DDD), Event Sourcing, and CQRS (Command Query Responsibility Segregation).

    While NestJS offers @nestjs/cqrs for basic CQRS, Ocoda Event Sourcing provides deeper integration for Event Sourcing and more comprehensive tools for DDD, such as aggregates, value objects, and repositories.

  2. What is Command Query Responsibility Segregation (CQRS)?

    master

    Command Query Responsibility Segregation (CQRS) is a design pattern that decouples the responsibility for modifying data from the responsibility for reading data. By separating these concerns, you can optimize the handling of commands and queries independently, allowing for better scalability, performance, and maintainability.

    Core Components

    • Commands: Represent actions or changes in the system (e.g., createOrder, updateProfile). They modify the system state but do not return data.
    • Queries: Used to retrieve data without modifying state (e.g., fetchProductList, getUserDetails). They focus solely on returning data.

    The Segregated Models

    • Write Model: Responsible for handling commands and enforcing business rules during state modifications.
    • Read Model: Optimized for queries, often using denormalized or cached data structures to provide fast and efficient retrieval.

    Key Characteristics

    • Eventual Consistency: Because the read and write models are often updated separately, there may be a short delay between a write operation and when that change is reflected in the read model.
    • Event Sourcing Integration: CQRS is frequently used alongside Event Sourcing, where state changes are stored as a sequence of events, allowing the system state to be rebuilt from the event log.
  3. What is Event Sourcing and how does it work?

    master

    Event Sourcing is a design pattern where the state of a system is derived from a sequence of immutable events rather than being stored directly. Instead of updating a record in a database, every state-changing operation is captured as an event in an append-only log. The current state is reconstructed by replaying these events in the order they occurred.

    To implement this pattern, you should understand these core components:

    • Events: Immutable facts representing actions that have already occurred (e.g., OrderCreated).
    • Event Store: An append-only persistent log used to record events.
    • Replaying Events: The process of rebuilding state by applying events in sequence.
    • Snapshots: An optimization technique where the current state is periodically saved so that replaying only needs to start from the last snapshot rather than the beginning of time.
    • Event Handlers: Components that react to events, such as updating read models or triggering side effects.

    Event Sourcing is frequently used in conjunction with CQRS (Command Query Responsibility Segregation) to separate the write model (which generates events) from the read model (which subscribes to events to update its state).

  4. How Commands and Command Handlers work together

    master

    In the CQRS pattern used by this library, Commands and Command Handlers separate intent from execution:

    1. Command: A simple data object representing a user or system's intent to change the state of the system (e.g., OpenAccountCommand). It should use imperative language and carry only the data required for the action.
    2. CommandHandler: A class containing the business logic required to execute a specific command. It interacts with domain aggregates and repositories to perform the action and persist changes.

    Commands are intended to trigger state changes, whereas queries (which are handled separately) are used to retrieve data.

    // A Command represents intent
    export class CreditAccountCommand implements ICommand {
    	constructor(
    		public readonly accountId: string,
    		public readonly amount: number,
    	) {}
    }
    
    // A CommandHandler executes the intent
    @CommandHandler(CreditAccountCommand)
    export class CreditAccountCommandHandler implements ICommandHandler {
    	async execute(command: CreditAccountCommand): Promise<boolean> {
    		// Logic to interact with aggregates and repositories goes here
    	}
    }
  5. What are Streams in Ocoda Event Sourcing

    master

    Streams are sequences of events or snapshots that belong to a specific aggregate. Each stream is identified by a unique streamId.

    • Event Streams: Represent the history of changes for an aggregate.
    • Snapshot Streams: Represent the state of an aggregate at a specific point in time.

    Stream names are automatically prefixed based on the name of the aggregate class (derived from the @Aggregate decorator), followed by the aggregate's unique ID.

  6. What are snapshots and when to use them

    master
    Snapshots are an optional optimization used to improve performance when event streams grow large. Instead of replaying every event from the beginning of a stream to reconstruct an aggregate's state, you can load a snapshot representing the aggregate at a specific point in time (revision) and then only replay the events that occurred after that snapshot. This significantly reduces the time required to load aggregates with long histories.
  7. What are Value Objects and how do they differ from Entities?

    master

    A Value Object represents a domain concept that is immutable and has no conceptual identity. Unlike entities, which are defined by a unique identity, value objects are defined purely by their attributes. They are used to describe characteristics of an entity and encapsulate logic related to validation or domain-specific constraints.

    Key characteristics:

    • No Identity: They are defined by their values, not a unique ID.
    • Immutability: Once created, they cannot be changed.
    • Equality: Two value objects are considered equal if all their properties are identical.
  8. Guidelines for creating events

    master

    When designing events for your domain, follow these three principles:

    1. Describe domain facts: Use domain-specific names that express what happened (e.g., AccountCreditedEvent) rather than generic state descriptions (e.g., BalanceChangedEvent).
    2. Ensure immutability: Once an event is created and persisted, it must never be changed. This allows the system to reliably replay events to rebuild state.
    3. Avoid logic in events: Events should be simple data structures. Do not include business logic within the event class, as this can cause unpredictable behavior during event replay.
  9. How to add logic to an Aggregate

    master

    Aggregates serve as the central point for enforcing business rules and managing consistency boundaries. When implementing logic, follow these core patterns:

    1. Mutate state through events: Do not modify properties directly. Instead, use this.applyEvent(new EventName(...)) to ensure changes are both applied to the current state and persisted to the event store.
    2. Enforce business invariants: Check business rules (e.g., checking for sufficient funds) before calling applyEvent. If a rule is violated, throw an error to prevent the event from being applied.
    3. Use expressive events: Name events based on what happened in the domain (e.g., AccountCreditedEvent) rather than generic state changes (e.g., BalanceChangedEvent).
    4. Handle events to update state: Use methods decorated with @EventHandler(EventClass) to define how the aggregate's state is mutated when a specific event is applied. This decouples business logic from state mutation.
    5. Maintain idempotency: Ensure event handlers can be safely replayed. Since aggregates are rehydrated by replaying events, handlers should check if a change has already been applied before mutating state to avoid duplicates.
    6. Focus on domain logic: Avoid infrastructure concerns like database access or HTTP calls inside the aggregate; delegate these to application services or repositories.
    // 1. Enforce invariant and mutate via event
    public debit(amount: number) {
        if (this.balance - amount < 0) {
            throw new Error('Insufficient funds');
        }
        this.applyEvent(new AccountDebitedEvent(amount));
    }
    
    // 2. Handle event to update state
    @EventHandler(AccountCreditedEvent)
    applyAccountCreditedEvent(event: AccountCreditedEvent) {
        this.balance += event.amount;
    }
    
    // 3. Ensure idempotency during replay
    onAccountOwnerAddedEvent(event: AccountOwnerAddedEvent) {
        if (!this.ownerIds.find(({ value }) => value === event.accountOwnerId)) {
            this.ownerIds.push(AccountOwnerId.from(event.accountOwnerId));
        }
    }
  10. Understand Event Serialization

    master

    By default, the library uses class-transformer to handle the serialization and deserialization of events. This is sufficient for events containing primitive types.

    If your events contain complex types, such as Value Objects, you must implement a custom EventSerializer to handle the transformation logic.

  11. Core concepts of Domain-Driven Design (DDD)

    master

    Domain-Driven Design (DDD) is a software development approach that aligns the software model with the core business domain. When using this library in a DDD context, you should structure your implementation around these key abstractions:

    • Domain: The central business problem being solved.
    • Domain Model: The code representation of business logic, rules, and constraints.
    • Ubiquitous Language: A shared vocabulary used by both developers and domain experts to ensure the software accurately reflects business needs.
    • Bounded Context: A boundary that defines where a specific model is applicable, preventing complexity by separating different parts of the system.
    • Entities: Objects with a unique, persistent identity (e.g., a User or an Order).
    • Value Objects: Immutable objects defined solely by their attributes, lacking a distinct identity (e.g., Money or a Date Range).
    • Aggregates: A cluster of related Entities and Value Objects treated as a single unit to maintain data consistency. The Aggregate acts as a consistency boundary.
    • Repositories: The mechanism used to retrieve and persist Aggregate roots.
    • Services: Logic that encapsulates domain processes that do not naturally belong to a single Entity or Value Object.
  12. How multitenancy works in Ocoda Event Sourcing

    master

    Ocoda Event Sourcing supports multitenancy by separating event and snapshot streams using pools. A pool is a collection of streams that belong to a specific tenant.

    By default, the library uses a shared collection of streams for all tenants. To implement multitenancy, you must disable the default pool in your configuration, explicitly create pools for each tenant using ensureCollection, and then specify the tenant's pool name whenever you interact with the eventStore or snapshotRepository.