From 8900a1560b71c2c6478a141cfb06654b08985dfe Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Sat, 10 Jan 2026 18:58:29 +0100 Subject: [PATCH] Add README with quick start example Add a README.md that gives developers a quick understanding of what Aether is and how to get started. Includes: - Project description and why Aether exists - Installation instructions - Quick start code example showing event creation, persistence, and replay - Key concepts (immutability, derived state, version consistency) - Links to further documentation - CI badge Closes #44 Co-Authored-By: Claude Opus 4.5 --- README.md | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e32a4c1 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Aether + +[![CI](https://git.flowmade.one/flowmade-one/aether/actions/workflows/ci.yml/badge.svg)](https://git.flowmade.one/flowmade-one/aether/actions/workflows/ci.yml) + +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 + +\`\`\`bash +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. + +\`\`\`go +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 + 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: + +\`\`\`go +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](./vision.md) - Product vision and design principles +- [CLAUDE.md](./CLAUDE.md) - Development guide and architecture details + +## License + +See [LICENSE](./LICENSE) for details.