Files
aether/examples
Claude Code d929729d79 fix: address review feedback
- Remove redundant newline from fmt.Println at line 325
- Add 9 comprehensive VersionConflictError tests to event_test.go:
  * Error message formatting with context fields
  * Field accessibility (ActorID, AttemptedVersion, CurrentVersion)
  * Unwrap() method and error wrapping
  * Sentinel error checking with errors.Is()
  * Type assertion support with errors.As()
  * Retry logic extraction of CurrentVersion
  * Special character handling in ActorID

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-01-13 22:24:37 +01:00
..

Aether Examples

Standard patterns and best practices for building with Aether.

Version Conflict Retry Patterns

When using optimistic concurrency control with Aether's event store, version conflicts can occur when multiple writers attempt to save events for the same actor. The VersionConflictError provides full context about the conflict, enabling intelligent retry strategies.

Understanding Version Conflicts

A version conflict occurs when:

  • You attempt to save an event with version N
  • But the actor already has a version >= N

Example:

// Actor "order-123" currently has version 5
// Writer A reads version 5, creates version 6, saves successfully
// Writer B also read version 5, creates version 6, attempts save
// -> VersionConflictError: current=6, attempted=6

Working with VersionConflictError

The VersionConflictError provides:

  • ActorID - The actor that had the conflict
  • CurrentVersion - The actual current version in the store
  • AttemptedVersion - The version you tried to save

Example usage:

err := eventStore.SaveEvent(event)
if errors.Is(err, aether.ErrVersionConflict) {
    var versionErr *aether.VersionConflictError
    if errors.As(err, &versionErr) {
        fmt.Printf("Conflict for actor %q: current=%d, attempted=%d",
            versionErr.ActorID, versionErr.CurrentVersion, versionErr.AttemptedVersion)
        // Implement retry logic using CurrentVersion
        nextVersion := versionErr.CurrentVersion + 1
    }
}
const maxRetries = 5
const baseDelay = 10 * time.Millisecond

for attempt := 0; attempt < maxRetries; attempt++ {
    currentVersion, _ := eventStore.GetLatestVersion(actorID)
    
    event := &aether.Event{
        ActorID: actorID,
        Version: currentVersion + 1,
        // ...
    }
    
    err := eventStore.SaveEvent(event)
    if err == nil {
        return nil  // Success!
    }
    
    if !errors.Is(err, aether.ErrVersionConflict) {
        return err  // Different error, don't retry
    }
    
    // Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms
    delay := time.Duration(baseDelay.Milliseconds() * int64(math.Pow(2, float64(attempt)))) * time.Millisecond
    time.Sleep(delay)
}
return fmt.Errorf("max retries exceeded")

Pros:

  • Simple to understand and implement
  • Respects store capacity
  • Good for most scenarios

Cons:

  • Can cause thundering herd in high-concurrency scenarios
  • May not work well if conflicts are due to logical issues

Pattern 2: State Reload and Merge

Use this pattern when you can merge concurrent changes:

const maxRetries = 3

for attempt := 0; attempt < maxRetries; attempt++ {
    // Reload current state
    events, _ := eventStore.GetEvents(actorID, 0)
    aggregate := rebuildFromEvents(events)
    
    // Apply your update
    aggregate.Status = "shipped"
    
    // Attempt save with new version
    event := &aether.Event{
        ActorID: actorID,
        Version: aggregate.Version + 1,
        Data: map[string]interface{}{"status": aggregate.Status},
    }
    
    err := eventStore.SaveEvent(event)
    if err == nil {
        return nil  // Success!
    }
    
    if !errors.Is(err, aether.ErrVersionConflict) {
        return err
    }
    
    // Reload and retry (loop continues)
}

Pros:

  • Deterministic - will eventually succeed
  • Can merge concurrent updates
  • Good for business logic that's idempotent

Cons:

  • More expensive (replaying events each attempt)
  • Only works if updates can be safely retried

Pattern 3: Circuit Breaker for Cascading Failures

Use when you want to avoid hammering a saturated store:

type CircuitBreaker struct {
    state                string        // "closed", "open", "half-open"
    failures             int
    failureThreshold     int
    lastFailureTime      time.Time
    cooldownTime         time.Duration
}

// ... implement circuit breaker logic ...

// Usage:
if !cb.canAttempt() {
    return fmt.Errorf("circuit breaker open")
}

err := eventStore.SaveEvent(event)
if err == nil {
    cb.recordSuccess()
} else if errors.Is(err, aether.ErrVersionConflict) {
    cb.recordFailure()
    if cb.failureCount >= cb.failureThreshold {
        cb.open()
    }
}

Pros:

  • Prevents cascading failures
  • Allows store recovery time
  • Good for distributed systems

Cons:

  • More complex implementation
  • May reject valid requests temporarily

Pattern 4: Jittered Backoff for High Concurrency

Add randomness to prevent thundering herd:

exponentialDelay := time.Duration(baseDelay.Milliseconds() * int64(math.Pow(2, float64(attempt)))) * time.Millisecond
jitter := time.Duration(rand.Int63n(int64(exponentialDelay)))
delay := exponentialDelay + jitter
time.Sleep(delay)

Pros:

  • Prevents synchronized retries
  • Good for high-concurrency scenarios

Cons:

  • Slightly more complex
  • May increase total retry time

Complete Example

See version_conflict_retry.go for complete, runnable examples of all patterns.

When to Use Each Pattern

Pattern Use When Avoid When
Exponential Backoff Default choice for most apps Store is consistently overloaded
State Reload Updates can be safely replayed Event replay is expensive
Circuit Breaker Store is frequently saturated You need immediate feedback
Jittered Backoff Many concurrent writers Single-threaded app

Monitoring Version Conflicts

Log and monitor version conflicts to understand contention patterns:

var versionErr *aether.VersionConflictError
if errors.As(err, &versionErr) {
    log.WithFields(log.Fields{
        "actor_id": versionErr.ActorID,
        "current_version": versionErr.CurrentVersion,
        "attempted_version": versionErr.AttemptedVersion,
        "version_gap": versionErr.AttemptedVersion - versionErr.CurrentVersion,
    }).Warn("Version conflict")
    
    // Alert if gap is too large (indicates stale read)
    if versionErr.AttemptedVersion - versionErr.CurrentVersion > 5 {
        metrics.versionConflictLargeGap.Inc()
    }
}

Best Practices

  1. Always check the error type - Not all errors are version conflicts
  2. Use CurrentVersion for retries - Don't hardcode retry logic
  3. Set reasonable retry limits - Prevent infinite loops
  4. Monitor contention - Track version conflicts to identify hotspots
  5. Consider your domain - Some updates can be safely retried, others cannot
  6. Test concurrent scenarios - Version conflicts are rare in single-threaded apps

References