Claude Code 69da1d800e
Some checks failed
CI / build (pull_request) Successful in 21s
CI / integration (pull_request) Failing after 2m0s
docs: Verify and document append-only event guarantees
This commit addresses issue #60 by documenting and enforcing the immutability
guarantees of the event store:

- Document that EventStore interface is append-only by design (no Update/Delete methods)
- Document the immutable nature of events once persisted as an audit trail
- Add JetStream stream retention policy configuration documentation
- Add comprehensive immutability test (TestEventImmutability_InMemory, TestEventImmutability_Sequential)
- Enhance InMemoryEventStore to deep-copy events, preventing accidental mutations
- Update README with detailed immutability guarantees and audit trail benefits

The EventStore interface intentionally provides no methods to modify or delete
events. Once persisted, events are immutable facts that serve as a tamper-proof
audit trail. This design ensures compliance, debugging, and historical analysis.

Acceptance criteria met:
- EventStore interface documented as append-only (event.go)
- JetStream retention policy configuration documented (store/jetstream.go)
- Test verifying events cannot be modified after persistence (store/immutability_test.go)
- README documents immutability guarantees (README.md)

Closes #60

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-01-13 21:26:16 +01: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 immutable facts about what happened in the domain. The EventStore interface is intentionally append-only: it provides no methods to update or delete events. Once persisted, an event can never be modified, deleted, or overwritten.

This design ensures:

  • Audit trail: Complete, tamper-proof history of all state changes
  • Compliance: Events serve as evidence for regulatory requirements
  • Debugging: Full context of how the system reached its current state
  • Analysis: Rich domain data for business intelligence and analysis

To correct application state, you append new events (e.g., a "Reversed" or "Corrected" event) rather than modifying existing events. This maintains a complete history showing both the original event and the correction.

The JetStream backing store uses a retention policy (default: 1 year) to automatically clean up old events, but events are never manually deleted through Aether.

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%