Equinox
.NET event sourcing library with CosmosDB, DynamoDB, EventStoreDB, message-db, SqlStreamStore and integration test backends. Focused at stream level; see https://github.com/jet/propulsion for cross-stream projections/subscriptions/reactions
Install / Use
/learn @jet/EquinoxREADME
Equinox

Equinox is a set of low dependency libraries that allow for event-sourced processing against stream-based stores handling:
- Snapshots
- Caching
- Optimistic concurrency control
Not a framework; you compose the libraries into an architecture that fits your apps' evolving needs.
It does not and will not handle projections and subscriptions. See Propulsion for that.
Table of Contents
- Getting Started
- Design Motivation
- Features
- Currently Supported Data Stores
- Components
- Overview
- Templates
- Samples
- Building
- Releasing
- FAQ
- Acknowledgements
- Further Reading
Getting Started
- If you want to start with code samples that run in F# interactive, there's a simple
Counterexample usingEquinox.MemoryStore - If you are experienced with event sourcing, CosmosDB and F#, you might gain most from this 100 LOC end-to-end example using CosmosDB
- If you are familiar with basic event sourcing mechanisms and want a meatier example of applying Equinox to a problem, Einar Norðfjörð's article, The Equinox Programming model walks through a complete end-to-end sample covering the key design considerations.
- If you are experienced with CosmosDB and something like CosmoStore, but want to understand what sort of facilities Equinox adds on top of raw event management, see the Access Strategies guide
Design Motivation
Equinox's design is informed by discussions, talks and countless hours of hard and thoughtful work invested into many previous systems, frameworks, samples, forks of samples, the outstanding continuous work of the EventStore founders and team and the wider DDD-CQRS-ES community. It would be unfair to single out even a small number of people despite the immense credit that is due. Some aspects of the implementation are distilled from Jet.com systems dating all the way back to 2013.
An event sourcing system usually needs to address the following concerns:
- Storing events with good performance and debugging capabilities
- Transaction processing
- Optimistic concurrency (handle loading conflicting events and retrying if another transaction overlaps on the same stream)
- Folding events into a State, updating as new events are added
- Decoding events using codecs and formats
- Framework and application integration
- Projections and Reactions
Designing something that supports all of these as a single integrated solution results in an inflexible and difficult to use framework. Thus, Equinox focuses on two central aspects of event sourcing: items 1 and 2 on the list above.
Of course, the other concerns can't be ignored; thus, they are supported via other libraries that focus on them:
- FsCodec supports encoding and decoding (concern 3)
- Propulsion supports projections and reactions (concern 5)
Integration with other frameworks (e.g., Equinox wiring into ASP.NET Core) is something that is intentionally avoided; as you build your application, the nature of how you integrate things will naturally evolve.
We believe the fact Equinox is a library is critical:
- It gives you the ability to pick your preferred way of supporting your event sourcing system.
- There's less coupling to worry about as your application evolves over time.
If you're looking to learn more about and/or discuss Event Sourcing and it's myriad benefits, trade-offs and pitfalls as you apply it to your Domain, look no further than the thriving 4000+ member community on the DDD-CQRS-ES Discord; you'll get patient and impartial world class advice 24x7 (there are #equinox, #eventstore and #sql-stream-store channels for questions or feedback). (invite link)
Features
- Designed not to invade application code; your domain tests can be written directly against your models.
- Core ideas and features of the library are extracted from ideas and lessons learned from existing production software.
- Test coverage for it's core features. In addition there are baseline and specific tests for each supported storage system and a comprehensive test and benchmarking story
- Pluggable event serialization. All encoding is specified in terms of the
FsCodec.IEventCodeccontract. FsCodec provides for pluggable encoding of events based on:NewtonsoftJson.Codec: a versionable convention-based approach (usingTypeshape'sUnionContractEncoderunder the covers), providing for serializer-agnostic schema evolution with minimal boilerplateSystemTextJson.Codec: a replacement to support Microsoft's default serializer - System.Text.Json.Box.Codec: lightweight non-serializing substitute equivalent toNewtonsoftJson.Codecfor use in unit and integration testsCodec: an explicitly coded pair ofencodeandtryDecodefunctions for when you need to customize
- Caching using the .NET
MemoryCacheto:- Minimize round trips; consistent implementation across stores :pray: @DSilence
- Minimize latency and bandwidth / Request Charges by maintaining the folded state, without needing the Domain Model folded state to be serializable
- Enable read through caching, coalescing concurrent reads via opt-in
LoadOption.AllowStale
- Mature and comprehensive logging (using Serilog internally), with optimal performance and pluggable integration with your apps hosting context (we ourselves typically feed log info to Splunk and the metrics embedded in the
Serilog.Events.LogEventProperties to Prometheus; see relevant tests for examples) - OpenTelemetry Integration (presently only implemented in
Equinox.CoreandEquinox.MessageDb...#help-wanted) Equinox.EventStore,Equinox.SqlStreamStore: In-stream Rolling Snapshots:- No additional round trips to the store needed at either the Load or Sync points in the flow
- Support for multiple co-existing compaction schemas for a given stream (A 'compaction' event/snapshot is an Event). This is done by the
FsCodec.IEventCodec- Compaction events typically do not get deleted (consistent with how EventStoreDB works), although it is safe to do so in concept (there are no assumptions that the events must be contiguous and/or that the number of events implies a specific version etc)
- While snapshotting can deliver excellent performance especially when allied with the Cache, it's not a panacea, as noted in this EventStore article on the topic
Equinox.MessageDb: Adjacent Snapshots:- Maintains snapshot events in an adjacent, separated
{Category}:snapshot-{StreamId}stream (in contrast to the EventStoreDb and SqlStreamStoreRollingStatestrategy, which embeds the snapshots directly within the stream in question) - Generating & storing the snapshot takes place subsequent to the normal appending of events, once every
batchSizeevents. This means the state of the stream can be reconstructed with exactly 2 round-trips to the database (caching can of course remove the snapshot reads on subsequent calls) - Note there'
- Maintains snapshot events in an adjacent, separated
