Emmett - Event Sourcing Framework

repository·main·Indexed 19 days ago

https://github.com/event-driven-io/emmett

A Node.js framework designed to simplify Event Sourcing development using pragmatic abstractions and strong TypeScript support. Emmett provides built-in support for multiple event stores including PostgreSQL, EventStoreDB, MongoDB, SQLite, and In-Memory storage, as well as projections for building read models, Express.js integration, and zero-configuration observability via OpenTelemetry.

Tokens
139.9K
Snippets
398
Records
545
Agent score
66%

What's inside Emmett

  1. Overview of Emmett for Event Sourcing

    main

    Emmett is an opinionated yet flexible framework designed to implement Event Sourcing in Node.js applications. It provides lightweight abstractions and clear patterns to make Event Sourcing accessible and maintainable, focusing on composition over 'magic'.

    Key benefits include:

    • Reduced Boilerplate: Pragmatic abstractions for common event-driven operations.
    • Observability: By keeping all system facts as events, it enables better business process observability and easier integration.
    • Built-in Infrastructure: Ready-to-use event store implementations and testing utilities.
  2. What is Emmett?

    main
    Emmett is an opinionated yet flexible framework designed to implement Event Sourcing for Node.js applications. It focuses on composition over magic, providing lightweight abstractions and clear patterns to make Event Sourcing accessible and maintainable. It is built with first-class TypeScript support and integrates with popular web frameworks like Express.js and Fastify.
  3. Explore the Emmett API reference

    main

    The Emmett API is built around four core building blocks that facilitate event-driven development and event sourcing. To build applications with Emmett, you will primarily interact with these abstractions:

    • Event: The fundamental unit of state change in the system.
    • Command: An intention to change state, which is processed by a handler.
    • Event Store: The persistence mechanism that stores the sequence of events.
    • Command Handler: The logic responsible for receiving commands, validating them, and producing events.
  4. Emmett Main Features

    main

    Emmett offers several key capabilities for building event-driven systems:

    • Event-Centric Modeling: Structured approach to modeling business processes through events.
    • Command Handling Patterns: Standardized approach using the Decider pattern.
    • Read Models: Built-in projections to build optimized read models from event streams.
    • Workflows: Support for durable execution to coordinate multi-step processes.
    • Web Framework Integration: Seamless support for Express.js and Fastify.
    • Comprehensive Testing: BDD-style testing using DeciderSpecification and ApiSpecification, along with TestContainers support for Docker-based testing.
  5. Explore Emmett Sample Applications

    main

    Emmett provides several complete sample applications implementing a Shopping Cart domain. These samples allow you to compare different combinations of web frameworks and event stores to find the right architecture for your needs.

    Available Samples

    SampleEvent StoreFrameworkDescription
    Express + PostgreSQLPostgreSQLExpress.jsRecommended starting point
    Hono + PostgreSQLPostgreSQLHonoHono and Node.js HTTP
    Express + MongoDBMongoDBExpress.jsDocument-oriented approach
    Express + EventStoreDBEventStoreDBExpress.jsNative ES capabilities

    All samples use the same domain model, making it easy to swap storage backends while keeping the business logic consistent.

  6. Explore Emmett core modules

    main

    The @event-driven-io/emmett package is organized into several functional modules:

    • commandHandling: Implementations for handling commands, including Decider-based handlers.
    • eventStore: Abstractions for event storage, including in-memory implementations and concurrency control.
    • projections: Tools for defining and running projections.
    • messageBus: Abstractions for CommandBus and EventBus.
    • workflows: Support for the Saga/workflow pattern via Workflow types.
    • processors: Tools like reactor and projector for processing messages.
    • database: Document database abstractions (e.g., InMemoryDatabase).
    • testing: BDD-style testing utilities like DeciderSpecification and assertions.
  7. What is a Decider and how does it work?

    main

    The Decider pattern is the core building block for event-sourced business logic. It separates business decisions from infrastructure concerns by using three pure functions to manage state transitions.

    A Decider consists of:

    • decide: A function that takes a Command and the current State, returning an array of Events. This is where business rules and validations are applied.
    • evolve: A function that takes the current State and an Event, returning a new State. This function should be simple and trust that the event has already been validated.
    • initialState: A function that returns the starting state of the aggregate.

    By keeping these functions pure, you can easily test business logic without needing a database or network connection.

    type Decider<State, CommandType extends Command, StreamEvent extends Event> = {
      decide: (command: CommandType, state: State) => StreamEvent | StreamEvent[];
      evolve: (currentState: State, event: StreamEvent) => State;
      initialState: () => State;
    };
  8. What is a Command in Emmett?

    main

    A Command represents an intention to perform a business operation. It is a request directed at a specific handler. Unlike Events, which are immutable facts about the past, Commands are imperative requests that may be rejected by the handler.

    AspectCommandEvent
    TenseImperative (e.g., AddProductItem)Past (e.g., ProductItemAdded)
    OutcomeMay be rejectedImmutable fact
    MultiplicitySingle handlerMultiple subscribers

    Commands express intent, and the handler decides whether to accept or reject the request based on the current state.

  9. Define routes using the WebApiSetup pattern

    main

    The recommended pattern for organizing routes is to create a function that returns a WebApiSetup function. This function accepts an Express Router and defines the routes. Inside the routes, use the on helper to wrap your asynchronous request handlers.

    import { on, ok, created, notFound } from '@event-driven-io/emmett-expressjs';
    import { Router } from 'express';
    
    export const shoppingCartApi = (eventStore: EventStore) => (router: Router) => {
      // GET - Read shopping cart
      router.get(
        '/carts/:cartId',
        on(async (request) => {
          const cartId = request.params.cartId;
          // ... logic to fetch state ...
          if (notFoundCondition) {
            return notFound({ detail: `Cart ${cartId} not found` });
          }
          return ok(state, { eTag: currentStreamVersion });
        }),
      );
    
      // POST - Add product item
      router.post(
        '/carts/:cartId/items',
        on(async (request) => {
          // ... logic to handle command ...
          return ok({ success: true }, { eTag: result.nextExpectedStreamVersion });
        }),
      );
    };
  10. Understand ReadEvent and event metadata

    main

    When reading events from an event store, the structure changes to include infrastructure-level metadata provided by the store.

    Stream Metadata

    Events read from a specific stream include position and timing information:

    • streamName: The name of the stream the event belongs to.
    • streamPosition: The 0-indexed position within the stream (bigint).
    • createdAt: When the event was recorded.

    Global Position

    Some stores provide a globalPosition (of type ProcessorCheckpoint) which represents the position across all streams.

  11. Choose between Inline and Async projections

    main

    Inline Projections

    Inline projections run within the same database transaction as the event append. This ensures strong consistency: either both the event is saved and the read model is updated, or both fail. Use when: Consistency is a priority. This is the recommended default for single-stream projections.

    Async Projections

    Async projections process events in a background process, decoupled from the event append. This results in eventual consistency, where the read model may lag slightly behind the event store. Use when:

    • You need faster write performance (appends).
    • You are projecting to external systems.
    • You have multi-stream projections that might suffer from concurrent write conflicts (where multiple streams updating the same document could overwrite each other).
  12. Related concepts for Deciders

    main

    For more information on implementing business logic and coordinating aggregates, refer to these resources:

    • Getting Started - Business Logic: Core concepts for implementing domain logic.
    • Command Handler: Learn how the CommandHandler uses a Decider internally to process incoming commands.
    • Testing Patterns: A comprehensive guide to testing your event-sourced logic.
    • Workflows: Guidance on coordinating multiple aggregates using workflows.