goes is an event-sourcing framework for Go. It gives you the building blocks to model domain logic with aggregates, persist state as events, build read models with projections, and wire the same application to in-memory or production backends.
goes currently targets Go 1.24+.
go get github.com/modernice/goes/...
The /... suffix downloads the framework packages together with the backend implementations.
This is the smallest useful path: define an aggregate, save it to an in-memory event store, then fetch it back by replaying events.
package main
import (
"context"
"fmt"
"log"
"github.com/google/uuid"
"github.com/modernice/goes/aggregate"
"github.com/modernice/goes/aggregate/repository"
"github.com/modernice/goes/event"
"github.com/modernice/goes/event/eventstore"
)
const (
listAggregate = "todo.list"
listCreated = "todo.list.created"
itemAdded = "todo.list.item_added"
)
type (
listCreatedEvent = event.Of[string]
itemAddedEvent = event.Of[string]
)
type List struct {
*aggregate.Base
Title string
Items []string
}
func NewList(id uuid.UUID) *List {
l := &List{Base: aggregate.New(listAggregate, id)}
event.ApplyWith(l, l.created, listCreated)
event.ApplyWith(l, l.added, itemAdded)
return l
}
func (l *List) Create(title string) {
aggregate.Next(l, listCreated, title)
}
func (l *List) AddItem(item string) {
aggregate.Next(l, itemAdded, item)
}
func (l *List) created(evt listCreatedEvent) {
l.Title = evt.Data()
}
func (l *List) added(evt itemAddedEvent) {
l.Items = append(l.Items, evt.Data())
}
func main() {
ctx := context.Background()
store := eventstore.New()
lists := repository.Typed(repository.New(store), NewList)
id := uuid.New()
list := NewList(id)
list.Create("Groceries")
list.AddItem("Milk")
list.AddItem("Eggs")
if err := lists.Save(ctx, list); err != nil {
log.Fatal(err)
}
fetched, err := lists.Fetch(ctx, id)
if err != nil {
log.Fatal(err)
}
fmt.Println(fetched.Title, fetched.Items)
}
What happens here:
List is an aggregate that raises events instead of mutating persisted state directlyaggregate.Next records and applies events immediatelyrepository.Typed(...) saves uncommitted events and reconstructs aggregates on fetcheventstore.New() keeps everything in memory, so you can prototype and test without infrastructureaggregate - define consistency boundaries that own state and business rulesevent - describe immutable facts and store or publish themaggregate/repository - save and rehydrate aggregates from an event storeprojection - build read-optimized views from event streamscommand - coordinate intent across aggregates and services when neededcodec - register event and command types for serialization across backendsApplication code stays on framework interfaces like event.Store and event.Bus. Pick the backend at startup.
| Component | In-memory | Production options |
|---|---|---|
| Event store | event/eventstore |
backend/mongo, backend/postgres |
| Event bus | event/eventbus |
backend/nats |
| Snapshots | - | backend/mongo |
| Read models | backend/memory |
backend/mongo |
Use the in-memory backends for tests and local experiments. Use MongoDB or PostgreSQL for persisted event streams and NATS for distributed event delivery.
Event-sourced aggregates are easy to test because they are in-memory state machines. goes also ships github.com/modernice/goes/exp/gtest for aggregate-focused assertions and provides in-memory backends for integration tests without external services.
docs/guide/testing.mdexp/gtestdocs/getting-started/docs/tutorial/docs/backends/docs/reference/examples/todo/