package optimistic
import (
"errors"
"sync"
)
type Action struct {
ID string
Apply func() error
Compensate func() error
}
type ActionLog struct {
mu sync.Mutex
applied []Action
}
func (l *ActionLog) Do(a Action) error {
if err := a.Apply(); err != nil {
return err
}
l.mu.Lock()
l.applied = append(l.applied, a)
l.mu.Unlock()
return nil
}
func (l *ActionLog) Rollback() error {
l.mu.Lock()
pending := l.applied
l.applied = nil
l.mu.Unlock()
var errs []error
for i := len(pending) - 1; i >= 0; i-- {
if err := pending[i].Compensate(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
package optimistic
import (
"errors"
"sync"
)
var ErrVersionConflict = errors.New("version conflict")
type Item struct {
Name string
Version int
}
type Store struct {
mu sync.Mutex
items map[string]Item
}
func NewStore() *Store {
return &Store{items: make(map[string]Item)}
}
func (s *Store) Update(id string, expectedVersion int, name string) (prev Item, err error) {
s.mu.Lock()
defer s.mu.Unlock()
cur := s.items[id]
if cur.Version != expectedVersion {
return Item{}, ErrVersionConflict
}
s.items[id] = Item{Name: name, Version: cur.Version + 1}
return cur, nil
}
func (s *Store) Restore(id string, snapshot Item) {
s.mu.Lock()
s.items[id] = snapshot
s.mu.Unlock()
}
package optimistic
import (
"encoding/json"
"errors"
"net/http"
)
type UpdateRequest struct {
ID string `json:"id"`
Name string `json:"name"`
ExpectedVersion int `json:"expected_version"`
}
type Handler struct {
Store *Store
Notify func(id, name string) error
}
func (h *Handler) OptimisticUpdate(w http.ResponseWriter, r *http.Request) {
var req UpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
log := &ActionLog{}
var snapshot Item
apply := Action{
ID: "update:" + req.ID,
Apply: func() error {
prev, err := h.Store.Update(req.ID, req.ExpectedVersion, req.Name)
snapshot = prev
return err
},
Compensate: func() error {
h.Store.Restore(req.ID, snapshot)
return nil
},
}
if err := log.Do(apply); err != nil {
if errors.Is(err, ErrVersionConflict) {
http.Error(w, "stale write", http.StatusConflict)
return
}
http.Error(w, "update failed", http.StatusInternalServerError)
return
}
if err := h.Notify(req.ID, req.Name); err != nil {
_ = log.Rollback()
http.Error(w, "downstream unavailable, reverted", http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]int{"version": req.ExpectedVersion + 1})
}
This snippet demonstrates the server side of an optimistic UI flow: the client mutates a resource immediately and the backend applies the change speculatively, keeping a durable trail of how to undo it if a later step fails. The pattern is a lightweight saga — every forward action carries a paired compensating action, so recovery is deterministic instead of ad-hoc.
In action_log.go, the core abstraction is Action, a pair of Apply and Compensate closures plus an ID. The ActionLog is an append-only, mutex-guarded stack of applied actions. Do runs Apply and, only on success, pushes the action so it can be reversed later. Rollback walks the log in reverse (LIFO) invoking each Compensate, which matters because compensations must unwind in the opposite order they were applied — reversing a later write before an earlier one avoids leaving the store in a half-consistent state. Compensation errors are collected with errors.Join rather than aborting, since best-effort unwind is usually preferable to stopping midway and leaking partial state.
store.go provides the resource being mutated. Store holds versioned Item values, and Update implements optimistic concurrency: the caller passes the expectedVersion, and a mismatch returns ErrVersionConflict. This version check is what makes the optimistic UI safe — if two clients raced, the stale write is rejected instead of silently clobbering. The returned prev value is captured so a compensating action can restore it exactly.
handler.go ties it together. OptimisticUpdate builds an Action whose Apply performs the versioned Update and whose Compensate restores the previous snapshot. It runs the update through log.Do, then simulates a downstream dependency via notifyDownstream; if that fails, log.Rollback reverses everything applied in the request and the handler returns 409 or 502 so the client can revert its optimistic render.
The key trade-off is that compensation is logical, not transactional — between apply and compensate another actor could observe the intermediate state, so this suits UI-level consistency rather than strict financial invariants. It shines when a mutation spans several independent systems that lack a shared transaction, and the expectedVersion guard keeps concurrent optimistic writes from corrupting each other.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.