DEV Community

Cover image for Don’t be a CRUD boomer πŸ‘¨β€πŸ¦³, check out this new Event Sourcing library!
Valentin BEGGI for Serverless By Theodo

Posted on

Don’t be a CRUD boomer πŸ‘¨β€πŸ¦³, check out this new Event Sourcing library!

Event Sourcing is hard πŸ§—β€β™‚οΈ

Implementing Event Sourcing in Typescript is painful. The power of this pattern is often counterbalanced by the complexity of its implementation and the difficulty of its maintainability 🀯. Key features like event replays can be tough to implement πŸ₯΅.

At Kumo, many of our projects used Event Sourcing, and we often stumbled upon the same issues again and again and ended up coding the same thing over and over. 🀑 It soon became obvious to us that developers needed new and better tools to do Event Sourcing on their Serverless projects.

❗ Event Sourcing is a pattern for storing data as a list of ordered events. If you are not familiar with this pattern, we invite you to read this Event Sourcing presentation.


We can do better

We thought that the best way to achieve our goal was to create an open-source library that would provide the following advantages:

  • πŸ’­ Abstractions first: Castore has been designed with flexibility in mind. It gives you abstractions that are meant to be used anywhere: React apps, containers, Lambdas... you name it!
  • ⛑️ Full type safety: We love type inference and we know you will too! The lack of decently typed Event Sourcing library was one of the main reasons we created Castore.
  • πŸ“– Best practices enforced: The Event Sourcing journey has many hidden pitfalls. We ran into them for you! Castore is opiniated. It comes with a collection of best practices and documented anti-patterns that we hope will help you out!

  • πŸ™…β€β™‚οΈ We do NOT deploy resources: While some packages like DynamoDBEventStorageAdapter require compatible infrastructure, Castore is not responsible for deploying it.


Introducing Castore 🦫

As a result, after months of work ✨, we are happy to introduce Castore!

Castore is an open-source Typescript library! It provides a set of classes, methods, and adapters to offer the best typescript developer experience for implementing Event Sourcing and interacting with any storage solution you want to use. V1 of Castore is already live! 🌟


Castore API βš™οΈ

1. EventStore πŸͺ

Castore provides you with an EventStore class. This is the entry point of Castore. This class exposes several methods to interact with your events and squeeze data out of them. πŸ–οΈπŸ‹πŸ–οΈ

To instantiate this class, you will need to define different event types. An event type is nothing more than a type-safe object describing the metadata and payload of your event.

You can use the EventType constructor πŸ‘· to create basic events or use more sophisticated JsonSchemaEventType or ZodEventType to create events with runtime validation! πŸ‘¨β€πŸ«

You will also need to provide a reducer when creating the EventStore. The reducer is the function that will be applied to your ordered events to build their summarized view which we call aggregate.

import { EventType } from "@castore/core"

export const userCreatedEvent = new EventType<
  // Typescript EventType
  'USER_CREATED',
  // Typescript EventDetails
  {
    aggregateId: string;
    version: number;
    type: 'USER_CREATED';
    timestamp: string;
    payload: { name: string; age: number };
  }
>({
  // EventType
  type: 'USER_CREATED',
});

const userEventStore = new EventStore ({
  eventStoreEvents: [userCreatedEvent],
  reducer:  (
    userAggregate: UserAggregate,
    event: UserEventsDetails,
  ): UserAggregate => {
    const { version, aggregateId } = event;

    switch (event.type) {
      case 'USER_CREATED': {
        const { name, age } = event.payload;

        return {
          aggregateId,
          version: event.version,
          name,
          age,
          status: 'CREATED',
        };
      }
      ...
    }
  }
  ...
});
Enter fullscreen mode Exit fullscreen mode

2. Storing data πŸ’½

Now that you have defined your EventStore and your types of Events, you want to store Events somewhere. We have created adapters to make your EventStore compatible with different data storage solutions. For now, DynamoDBAdapter and InMemoryAdapter are the two StorageAdapters available with Castore. But you can of course create your own if you want! πŸ”¨

const userEventStore = new EventStore({
  eventStoreEvents: [...], 
  storageAdapter: new DynamoDbEventStorageAdapter({
    tableName: "MyTable", 
    dynamoDbClient: new DynamoDBClient()
  })
})
Enter fullscreen mode Exit fullscreen mode

3. Interacting with the EventStore 🧠

To push and retrieve events from the event store, the EventStore class exposes methods like pushEvent or getAggregate. These methods use the adapter to interact with the data storage entity and add or retrieve events.

Here is a quick example showing how an application would use these two methods:

const removeUser = async (userId: string) => {
  // get the aggregate for that userId,
  // which is a representation of our user's state
  const { aggregate } = await userEventStore.getAggregate(userId);

  // use the aggregate to check the user status
  if (aggregate.status === 'REMOVED') {
    throw new Error('User already removed');
  }

  // put the USER_REMOVED event in the event store 🦫
  await userEventStore.pushEvent({
    aggregateId: userId,
    version: aggregate.version + 1,
    type: 'USER_REMOVED',
    timestamp: new Date().toIsoString(),
  });
};
Enter fullscreen mode Exit fullscreen mode

Go check out the repo to find out about other cool features of Castore including Commands, Snapshots, custom mock helpers... πŸ’«


Conclusion

Castore makes the Typescript developer experience so easy it should be illegal. You will be interacting with your event store like never before, optimization and best practices being abstracted away to let you focus on delivering business value as fast and seamlessly as possible. 🍰

We can’t wait to get your feedback ⭐ and add more features to Castore. The incoming features include events migration πŸ•ŠοΈ, Projection classes πŸ“½οΈ... Feel free to contribute !

As a bonus, if you are a Serverless fanboy, here is a cool article to get a better grasp on serverless Event Sourcing architectures and their problematics written by one of our lovely colleagues at Theodo.

Written with ❀️ by @julietteff @charlesgery @thomasaribart @valentinbeggi

Top comments (3)

Collapse
 
thomasaribart profile image
Thomas Aribart

Hello Tamas and thanks for the comment !

I agree that it adds complexity and costs. However:

  • Some business domains like banking, insurance, betting platforms etc... may simply require event sourcing
  • Event sourcing works very well with event driven architectures
  • Once you get used to it, it also becomes very natural to use for developers! Especially in complex systems in which there is many ways to udpate a resource, the REST paradigm can become a bit awkward, and the do_something command + something_done event is way more fitting. I have grown to love it!

That being said, there are also situations where Event Sourcing is not the right tool. For instance, events need to be actual business events, not UI or analytics related stuff, or saving the intermediate state of a long form.

Hope it answers your question :)

Collapse
 
rzulty profile image
Piotr Grzegorzewski

How does it relate to #aws? Is it something specific that you did to support it over other cloud providers?

Collapse
 
valentinbeggi profile image
Valentin BEGGI

Hi Piotr !

At Kumo, our preferred data storage solution for our events is DynamoDB. It's snappy to get data from it and events do not have complex access patterns ⚑.

For this reason, we built a DynamoDBAdapter to Castore. It is the only DB Adapter so-far (except for local storage) and that's why we mentioned #AWS !

Castore itself is stack agnostic though πŸŽ‰ !