Claude Code 464fed67ec
Some checks failed
CI / build (pull_request) Successful in 23s
CI / build (push) Successful in 21s
CI / integration (push) Has been cancelled
CI / integration (pull_request) Failing after 2m2s
feat(event-sourcing): Publish EventStored after successful SaveEvent
Add EventStored internal event published to the EventBus when events are
successfully persisted. This allows observability components (metrics,
projections, audit systems) to react to persisted events without coupling
to application code.

Implementation:
- Add EventTypeEventStored constant to define the event type
- Update InMemoryEventStore with optional EventBroadcaster support
- Add NewInMemoryEventStoreWithBroadcaster constructor
- Update JetStreamEventStore with EventBroadcaster support
- Add NewJetStreamEventStoreWithBroadcaster constructor
- Implement publishEventStored() helper method
- Publish EventStored containing EventID, ActorID, Version, Timestamp
- Only publish on successful SaveEvent (not on version conflicts)
- Automatically recorded in metrics through normal Publish flow

Test coverage:
- EventStored published after successful SaveEvent
- No EventStored published on version conflict
- Multiple EventStored events published in order
- SaveEvent works correctly without broadcaster (nil-safe)

Closes #61

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-01-13 21:39:21 +00:00
2026-01-10 22:52:35 +00:00
2026-01-08 19:30:02 +01:00
2026-01-08 19:30:02 +01:00
2026-01-08 19:30:02 +01:00

Aether

CI

Event sourcing primitives for Go, powered by NATS.

Aether provides composable building blocks for distributed, event-sourced systems without imposing framework opinions on your domain.

Why Aether?

Building distributed, event-sourced systems in Go requires assembling many pieces: event storage, pub/sub, clustering, leader election. Existing solutions are either too heavy (full frameworks with opinions about your domain), too light (just pub/sub), or not NATS-native.

Aether provides clear primitives that compose well:

  • Event sourcing primitives - Event, EventStore interface, snapshots
  • Event stores - In-memory (testing) and JetStream (production)
  • Event bus - Local and NATS-backed pub/sub with namespace isolation
  • Cluster management - Node discovery, leader election, shard distribution

Built for JetStream from the ground up, not bolted on.

Installation

go get git.flowmade.one/flowmade-one/aether

Requires Go 1.23 or later.

Quick Start

Here is a minimal example showing event sourcing fundamentals: creating events, saving them to a store, and replaying to rebuild state.

package main

import (
	"fmt"
	"time"

	"github.com/google/uuid"
	"git.flowmade.one/flowmade-one/aether"
	"git.flowmade.one/flowmade-one/aether/store"
)

func main() {
	// Create an in-memory event store (use JetStream for production)
	eventStore := store.NewInMemoryEventStore()

	// Create and save events
	// Error handling omitted for brevity
	orderID := "order-123"

	orderPlaced := &aether.Event{
		ID:        uuid.New().String(),
		EventType: "OrderPlaced",
		ActorID:   orderID,
		Version:   1,
		Data:      map[string]interface{}{"total": 99.99, "items": 3},
		Timestamp: time.Now(),
	}
	eventStore.SaveEvent(orderPlaced)

	orderShipped := &aether.Event{
		ID:        uuid.New().String(),
		EventType: "OrderShipped",
		ActorID:   orderID,
		Version:   2,
		Data:      map[string]interface{}{"carrier": "FastShip", "tracking": "FS123456"},
		Timestamp: time.Now(),
	}
	eventStore.SaveEvent(orderShipped)

	// Replay events to rebuild state
	events, _ := eventStore.GetEvents(orderID, 0)

	state := make(map[string]interface{})
	for _, event := range events {
		switch event.EventType {
		case "OrderPlaced":
			state["total"] = event.Data["total"]
			state["items"] = event.Data["items"]
			state["status"] = "placed"
		case "OrderShipped":
			state["status"] = "shipped"
			state["carrier"] = event.Data["carrier"]
			state["tracking"] = event.Data["tracking"]
		}
	}

	fmt.Printf("Order state after replaying %d events:\n", len(events))
	fmt.Printf("  Status: %s\n", state["status"])
	fmt.Printf("  Total: $%.2f\n", state["total"])
	fmt.Printf("  Tracking: %s\n", state["tracking"])
}

Output:

Order state after replaying 2 events:
  Status: shipped
  Total: $99.99
  Tracking: FS123456

Key Concepts

Events are immutable

Events represent facts about what happened. Once saved, they are never modified - you only append new events.

State is derived

Current state is always derived by replaying events. This gives you a complete audit trail and the ability to rebuild state at any point in time.

Versions ensure consistency

Each event for an actor must have a strictly increasing version number. This enables optimistic concurrency control:

currentVersion, _ := eventStore.GetLatestVersion(actorID)

event := &aether.Event{
	ActorID: actorID,
	Version: currentVersion + 1,
	// ...
}

err := eventStore.SaveEvent(event)
if errors.Is(err, aether.ErrVersionConflict) {
	// Another writer saved first - reload and retry
}

Documentation

  • Vision - Product vision and design principles
  • CLAUDE.md - Development guide and architecture details

License

See LICENSE for details.

Description
Distributed actor system with event sourcing for Go
Readme 800 KiB
Languages
Go 100%