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>
153 lines
4.7 KiB
Markdown
153 lines
4.7 KiB
Markdown
# Aether
|
|
|
|
[](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
|
|
// 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:
|
|
|
|
```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.
|