synapse
A Go event sourcing and CQRS toolkit. Composable primitives — aggregates, events, repositories, projections — plus the operational surface real services need: a Postgres backend, broker delivery for projections, sagas with timeouts, dead-letter handling, crypto-shredding, OpenTelemetry hooks, and an admin RPC for dump/restore.
Zero third-party dependencies in the root module. Anything that pulls in an external library (a SQL driver, a broker client, gRPC) lives as a sibling module so you opt in to what you use.
Status
Pre-1.0. The public surface is still moving; expect minor breaking changes between tagged versions. The Postgres and SQLite stacks (events + snapshots + checkpoints + dead-letter + timeouts + keystore, one pool / one file) are shaping up production-ready but have not yet seen production traffic.
Install
go get github.com/ianunruh/synapse
Requires Go 1.26 or newer.
Quick look
package main
import (
"context"
jsoncodec "github.com/ianunruh/synapse/codec/json"
"github.com/ianunruh/synapse/es"
"github.com/ianunruh/synapse/eventstore/memory"
)
// Define an aggregate by embedding *es.AggregateBase.
type Counter struct {
*es.AggregateBase
Value int
}
func NewCounter(id es.StreamID) *Counter {
return &Counter{AggregateBase: es.NewAggregateBase(id)}
}
type CounterIncremented struct {
By int `json:"by"`
}
func (c *Counter) Apply(env es.Envelope) {
if inc, ok := env.Payload.(CounterIncremented); ok {
c.Value += inc.By
}
}
func (c *Counter) Increment(by int) {
c.Record("counter.incremented", CounterIncremented{By: by}, c.Apply)
}
func main() {
ctx := context.Background()
reg := es.NewRegistry()
es.Register(reg, "counter.incremented", jsoncodec.For[CounterIncremented]())
repo := es.NewRepository(memory.New(), reg, NewCounter)
c := NewCounter("counter/hits")
c.Increment(2)
c.Increment(3)
_ = repo.Save(ctx, c)
loaded, _ := repo.Load(ctx, "counter/hits")
_ = loaded.Value // == 5
}A full walkthrough is in docs/getting-started.md. Runnable examples live under examples/.
What's in the box
Beyond the core aggregate/repository/projection trio, the toolkit ships:
-
Storage backends — Postgres (ADR-0024) and SQLite (ADR-0017) for the event store, snapshot store, checkpoint store, dead-letter store, timeout store, and key store. Postgres uses a shared
LISTEN/NOTIFY(ADR-0025) for live-tail; read-replica routing is opt-in (ADR-0038). -
Commands — Typed
es.Handler[C, A]+es.Execute(ADR-0009) for in-process callers;es/commandbusfor dispatching named, byte-encoded commands from HTTP/gRPC transports (ADR-0028). -
Process managers and sagas —
es/process(ADR-0032) wraps a saga as a projection that emits commands. Saga timeouts are scheduled via outbox-style intent events on the saga's own stream, atomic with Save (ADR-0043). -
Broker delivery —
outbox.Publisher(ADR-0042) + a NATS JetStream adapter;outbox/nats.Consumer(ADR-0044) lets projections subscribe to NATS instead of polling Postgres. End-to-end demo inexamples/nats-postgres-projection. -
Dead-letter queue —
projection.WithDeadLetter(ADR-0041) routes failed events to a persistent DLQ; the runner logs at Warn and continues. Admin RPCs and CLI subcommands surface entries for operators. -
Crypto-shredding — Per-subject AES-256-GCM via a
KeyStoreinterface (ADR-0036). Shredding a key tombstones every event for that subject in place; the rest of the log keeps replaying. -
Observability —
slogeverywhere by default (ADR-0015); an opt-inotelsibling module emits spans + duration histograms around every persistence call (ADR-0034). -
Admin RPCs —
synapse-adminand a gRPC service expose stream/checkpoint/snapshot/deadletter inspection plus dump/restore in JSONL (ADR-0033, ADR-0040). -
Liveness/readiness probes —
synapse/healthaggregates user-registered checks behind two HTTP handlers; stdlib only (ADR-0039). -
Event upcasters — Old event shapes evolve forward;
Applyonly ever sees the latest version (ADR-0023).
Documentation
- Getting started — a 20-minute walkthrough that builds the Counter aggregate above into a service with commands, a projection, snapshots, and a SQLite backend.
- Architecture Decision Records — 45 records explaining why the library is shaped the way it is.
- Benchmarks — baseline numbers for the core hot paths and the included store backends.
- Releasing — the multi-module tagging procedure used for each cut.
- Package docs: pkg.go.dev/github.com/ianunruh/synapse/es.
Packages
The repo is a Go workspace. Core interfaces live in es; backends and contract suites are sibling packages — or sibling modules when they pull in third-party deps.
| Package | Module | What it is |
|---|---|---|
| Core | ||
es |
root | Aggregate, Repository, Event, Envelope, Source, Snapshotter, Projection, codec Registry, error types |
es/middleware |
root | Built-in repository middleware: PerAggregateLocking, Retry |
es/projection |
root | Runner for read-model projections (type filters, batched checkpoints, dead-letter routing) |
es/commandbus |
root | Transport-facing CommandBus + middleware (Logging, Recover, Timeout) |
es/process |
root | Process managers / sagas with timeouts (intent events, TimeoutManager) |
idgen |
root | UUIDv7 identifier generator |
health |
root | Liveness + readiness HTTP probes |
crypto |
root | Per-subject AES-256-GCM crypto-shredding (stdlib only) |
| Codecs | ||
codec/json |
root | JSON event/snapshot codec |
codec/proto |
sibling | Protobuf event/snapshot codec |
codec/codectest |
root | Codec contract test suite |
| Event store | ||
eventstore/memory |
root | In-memory event store |
eventstore/postgres |
sibling | Postgres event store (pgxpool, shared LISTEN/NOTIFY, read-replica routing) |
eventstore/sqlite |
sibling | SQLite event store |
eventstore/eventstoretest |
root | Backend contract test suite |
| Snapshot store | ||
snapshotstore/memory |
root | In-memory snapshot store |
snapshotstore/postgres |
sibling | Postgres snapshot store |
snapshotstore/sqlite |
sibling | SQLite snapshot store |
snapshotstore/snapshotstoretest |
root | Backend contract test suite |
| Checkpoint store | ||
checkpointstore/memory |
root | In-memory checkpoint store |
checkpointstore/postgres |
sibling | Postgres checkpoint store |
checkpointstore/sqlite |
sibling | SQLite checkpoint store |
checkpointstore/checkpointstoretest |
root | Backend contract test suite |
| Dead-letter store | ||
deadletterstore/memory |
root | In-memory dead-letter store |
deadletterstore/postgres |
sibling | Postgres dead-letter store |
deadletterstore/sqlite |
sibling | SQLite dead-letter store |
deadletterstore/deadletterstoretest |
root | Backend contract test suite |
| Timeout store (saga deadlines) | ||
timeoutstore/memory |
root | In-memory timeout store |
timeoutstore/postgres |
sibling | Postgres timeout store |
timeoutstore/sqlite |
sibling | SQLite timeout store |
timeoutstore/timeoutstoretest |
root | Backend contract test suite |
| Key store (for crypto-shredding) | ||
keystore/memory |
root | In-memory key store |
keystore/postgres |
sibling | Postgres key store |
keystore/sqlite |
sibling | SQLite key store |
crypto/keystoretest |
root | Backend contract test suite |
| Outbox | ||
outbox |
root |
Publisher interface, AsProjection, Retrying
|
outbox/nats |
sibling | NATS JetStream Publisher + Consumer adapter |
| Observability | ||
otel |
sibling | OpenTelemetry store wrappers + projection.Runner tracing |
| Admin / ops | ||
admin |
sibling | gRPC admin service + synapse-admin CLI (dump/restore, deadletter, etc.) |
pgtest |
sibling | Postgres testing harness (testcontainers-go) |
| Examples | ||
examples/counter |
root | In-memory event sourcing walkthrough |
examples/order |
root | Multi-stage aggregate with command validation |
examples/projection |
root | Projection runner walkthrough |
examples/process |
root | Process manager driving a saga |
examples/saga-timeout |
root | Saga deadlines via outbox-style intent events |
examples/persistent |
sibling | SQLite-backed end-to-end demo |
examples/postgres |
sibling | Postgres-backed end-to-end demo |
examples/http-service |
sibling | CommandBus driven by net/http, live projection |
examples/nats-postgres-projection |
sibling | Postgres event store + NATS broker + Postgres read-model table |
Sibling modules each have their own go.mod. A go.work file at the repo root ties them together for local development. The root module has zero third-party deps; the SQLite backends transitively pull in modernc.org/sqlite (pure Go, no CGo).
Design principles
Recorded as Architecture Decision Records (starting at ADR-0001):
- Go 1.26 toolchain, language features and stdlib used to current capability.
- Zero third-party deps in the root module. Backends that need them live as sibling Go modules.
- Modernization-clean.
gopls modernize ./...exits 0 in every module. - Serialization-agnostic core. Codecs are registered per event type; the
espackage never imports a specific codec. - Type safety and performance are co-equal goals. Where they point the same direction (most cases), take both. Where they conflict, prefer the perf-friendly option in hot paths and document the trade-off.
- Admin RPCs and a web UI, when they exist, will be optional sibling subpackages users opt into.
License
Apache 2.0. See LICENSE.