Quick Start
This example builds a simple todo list aggregate — you can create a list, add items, and complete them. It saves to an in-memory event store and fetches it back. No external dependencies required.
The Aggregate
go
package todo
import (
"fmt"
"github.com/google/uuid"
"github.com/modernice/goes/aggregate"
"github.com/modernice/goes/event"
)
// Aggregate name.
const ListAggregate = "todo.list"
// Event names.
const (
ListCreated = "todo.list.created"
ItemAdded = "todo.list.item_added"
ItemCompleted = "todo.list.item_completed"
)
// Event data types.
type (
ListCreatedEvent = event.Of[string]
ItemAddedEvent = event.Of[string]
ItemCompletedEvent = event.Of[string]
)
// ListDTO holds the read state of a todo list.
// Including the ID makes the DTO self-contained — when you pass it to
// templates, APIs, or other packages, the ID is always available.
type ListDTO struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Items map[string]bool `json:"items"` // item title -> completed
}
// Created reports whether the list has been initialized.
func (dto ListDTO) Created() bool {
return dto.Title != ""
}
// Print writes the list to stdout.
func (dto ListDTO) Print() {
fmt.Println(dto.Title)
for item, done := range dto.Items {
check := "[ ]"
if done {
check = "[x]"
}
fmt.Printf(" %s %s\n", check, item)
}
}
// List is an event-sourced todo list.
type List struct {
*aggregate.Base
ListDTO
}
// NewList creates a new todo list aggregate.
func NewList(id uuid.UUID) *List {
l := &List{
Base: aggregate.New(ListAggregate, id),
ListDTO: ListDTO{
ID: id,
Items: make(map[string]bool),
},
}
event.ApplyWith(l, l.created, ListCreated)
event.ApplyWith(l, l.itemAdded, ItemAdded)
event.ApplyWith(l, l.itemCompleted, ItemCompleted)
return l
}
// Create initializes the list with a title.
func (l *List) Create(title string) error {
if l.Created() {
return fmt.Errorf("list already created")
}
aggregate.Next(l, ListCreated, title)
return nil
}
func (l *List) created(evt ListCreatedEvent) {
l.Title = evt.Data()
}
// AddItem adds a new item to the list.
func (l *List) AddItem(title string) error {
if !l.Created() {
return fmt.Errorf("list not created")
}
if _, exists := l.Items[title]; exists {
return fmt.Errorf("item %q already exists", title)
}
aggregate.Next(l, ItemAdded, title)
return nil
}
func (l *List) itemAdded(evt ItemAddedEvent) {
l.Items[evt.Data()] = false
}
// CompleteItem marks an item as done.
func (l *List) CompleteItem(title string) error {
if !l.Created() {
return fmt.Errorf("list not created")
}
done, exists := l.Items[title]
if !exists {
return fmt.Errorf("item %q not found", title)
}
if done {
return fmt.Errorf("item %q already completed", title)
}
aggregate.Next(l, ItemCompleted, title)
return nil
}
func (l *List) itemCompleted(evt ItemCompletedEvent) {
l.Items[evt.Data()] = true
}Running It
go
package main
import (
"context"
"fmt"
"log"
"github.com/google/uuid"
"example/todo"
"github.com/modernice/goes/aggregate/repository"
"github.com/modernice/goes/event/eventstore"
)
func main() {
ctx := context.Background()
// Set up the event store and repository.
store := eventstore.New()
lists := repository.Typed(repository.New(store), todo.NewList)
// Create a list and add some items.
id := uuid.New()
list := todo.NewList(id)
if err := list.Create("Groceries"); err != nil {
log.Fatal(err)
}
list.AddItem("Milk")
list.AddItem("Eggs")
list.CompleteItem("Milk")
// Save — uncommitted events are inserted into the store.
if err := lists.Save(ctx, list); err != nil {
log.Fatal(err)
}
// Fetch — events are replayed to reconstruct state.
fetched, err := lists.Fetch(ctx, id)
if err != nil {
log.Fatal(err)
}
fetched.Print()
// Output:
// Groceries
// [x] Milk
// [ ] Eggs
}What Just Happened?
- Defined an aggregate —
Listembeds*aggregate.Base(which provides event-sourcing mechanics) and aListDTOthat holds the actual state. - Registered event handlers —
event.ApplyWithconnects event names to handler methods. The handler methods are unexported and only mutate state. They never validate or trigger side effects. - Raised events — Domain methods like
CreateandAddItemvalidate the operation, then callaggregate.Nextto record what happened. The event is immediately applied to the aggregate. - Saved and fetched — The repository inserted the uncommitted events into the store. On fetch, it replayed them through the handlers to reconstruct state.
Next Steps
Ready to build something real? The tutorial walks you through building a complete e-commerce application with products, orders, customers, and projections.