A streaming data processing engine for Go — with live schema evolution and an AI debugger that explains what's happening in plain language.
# Homebrew (macOS / Linux)
brew install gribovan2005/drift/drift
drift version
# or as a Go library
go get github.com/gribovan2005/drift/sdk
# or run the demo from source
go run ./cmd/demo # → http://localhost:8080Flink is powerful but heavy: JVM, ZooKeeper, cluster ops, schema change = job restart. Drift is the opposite — a single Go binary, zero external dependencies for the core, and two features Flink doesn't have:
| Drift | Flink | |
|---|---|---|
| Live Schema Evolution | Operators adapt mid-stream, zero downtime | Requires job restart |
| AI Debugger | Claude explains bottlenecks in plain language | Manual metrics analysis |
| Deployment | Single binary | JVM + cluster + ZooKeeper |
| Startup | < 50ms | Seconds to minutes |
| Distributed execution | Single process (MVP) | Horizontal scaling |
| Exactly-once | — | Chandy-Lamport checkpointing |
Use Drift when your schema changes frequently, you want a zero-infra streaming layer inside a Go service, or you need to understand what your pipeline is doing without a PhD in distributed systems.
Use Flink when you need petabyte-scale, exactly-once guarantees, or distributed fault tolerance.
# Build from source
go build ./cmd/demo
./demo
# → http://localhost:8080
# With AI debugging
ANTHROPIC_API_KEY=sk-ant-... ./demo
# → click "Ask Claude" in the UIThe demo runs a payment-processing pipeline at ~500 records/sec.
After 30 seconds, schema v2 is published live — watch the Schema Evolution panel update with zero downtime.
go run ./cmd/referencedemo # evolving stream → SchemaAdapter → fast-lane windowed agg → sink
go run ./cmd/schemademo # live schema evolution (rename + retype + add) in 5 linesreferencedemo ties the whole story together: an evolving order stream flows through the live SchemaAdapter into the columnar fast lane (windowed revenue per merchant), the schema is evolved mid-stream (a field added, no restart), and it prints throughput + windowed results — every record aggregated across the schema boundary.
Embed Drift directly in a Go service — one import, a fluent builder:
go get github.com/gribovan2005/drift/sdkimport "github.com/gribovan2005/drift/sdk"
out := sdk.Collect()
err := sdk.New().
From(sdk.Slice(in)). // or Kafka/HTTP/Generate
Filter(func(r sdk.Record) bool { return r.Payload["v"].(int)%2 == 0 }).
Map(func(r sdk.Record) (sdk.Record, error) {
r.Payload["v"] = r.Payload["v"].(int) + 1
return r, nil
}).
Tumbling(64, aggregate). // windows, dedup, joins…
To(out). // or Kafka/File/HTTP
Run(ctx)The sdk package is a thin facade over pkg/*; use Apply(op) for any operator
without a dedicated method, and Build() to get the raw *pipeline.Pipeline for
monitoring. See drift/Specs/SDK.md.
Observability: expose per-stage metrics to your existing Prometheus — no agent, no cluster:
p, _ := sdk.New().From(src).Map(fn).To(sink).Build()
http.Handle("/metrics", sdk.PrometheusHandler(p)) // drift_stage_processed_total, …
go p.Run(ctx)Runnable example — a real-time payment-analytics service in one file (live materialized view over HTTP, live schema evolution, Prometheus metrics):
go run ./cmd/sdkdemo
curl localhost:8090/stats # live window aggregates, no database
curl localhost:8090/metrics # Prometheus scrape
# at t+15s the schema evolves live — watch "schema_has_risk_score" flip to trueDrift has a vectorized fast-lane (pkg/vector) for the stateless hot path:
columnar Batches of typed values (no map[string]any, no boxing, no per-row GC),
processed in tight per-column loops and carried through the normal pipeline as
chunk-records. On the same Filter(even)+Map(+1) workload it is ~247× faster
than the row engine, and over real Kafka (10 partitions, binary columnar codec)
it sustains ~52M rows/s end-to-end. Full numbers + honest caveats in
BENCHMARKS.md.
| Lever | API | Effect |
|---|---|---|
| Columnar vectorized ops | vector.MapInt64/FilterInt64/Sum… |
~247× on Int64/Float64 Map/Filter; String/Bool + Sum/Count/Max too |
| Binary columnar codec | vector.EncodeBatch/DecodeBatch, vector.KafkaColumnarSource |
decode off the critical path (vs JSON) |
| Parallel ingestion | sdk.ParallelSource, sdk.KafkaPartitions |
read N partitions/shards concurrently |
| Per-stage parallelism | vector.Parallel(n, mk) / pipeline.Parallel |
scale a CPU-heavy stage across cores (~5.8× @8) |
| Resource profiles | sdk.WithProfile(sdk.Sidecar / sdk.Dedicated) |
tune batch/buffer/linger (+ runtime knobs when it owns the process) |
Runnable demos: cmd/sdkdemo (live analytics), cmd/e2ebench (in-process
JSON-vs-binary), cmd/kafkademo (real Kafka end-to-end). Scope is honest — the
fast-lane covers stateless transforms + simple aggregates; windows/joins stay on
the row engine. See drift/Specs/Vectorized Fast-Lane.md.
Source → [Stage 1] → [Stage 2] → … → Sink
Each stage is an Operator running in its own goroutine, connected by buffered channels. Backpressure is implicit.
type Record struct {
SchemaID string
SchemaVersion int
Payload map[string]any
}type Operator interface {
Process(in []Record) ([]Record, error)
OnSchemaChange(s Schema) // called by SchemaRegistry on new version
}Drift's headline feature. Publish a new schema version at runtime — subscribed operators receive OnSchemaChange immediately and adapt with the next batch. No restart, no state migration. The SchemaAdapter reshapes records to the current schema: it adds new fields (with defaults), drops removed fields, renames via an alias map, and coerces each value to its new type — so even a column type change (e.g. int → float) takes effect mid-stream.
reg := schema.NewRegistry()
reg.Register(v1)
adapter := operator.NewSchemaAdapter(v1, AliasMap{"amount": "amount_usd"})
reg.Subscribe("payments", adapter)
// Later, mid-stream — the running pipeline never stops:
reg.Register(v2) // → adapter.OnSchemaChange(v2) called automaticallySee it in 5 lines of output:
$ go run ./cmd/schemademo
── v1 (amount: int) ────────────────────────────────
v1 amount=1299(int64) merchant=acme(string)
── v2 (live: amount→amount_usd, int→float, +region) — no restart ──
v2 amount_usd=1299(float64) merchant=acme(string) region=us(string)
Same producer input, evolved live: the field was renamed, retyped int → float, and a new region field appeared — with zero downtime.
| Operator | Description |
|---|---|
Map(fn) |
1-to-1 transform |
Filter(pred) |
Keep records matching predicate |
FlatMap(fn) |
1-to-N (or filtering by returning nil) |
SchemaAdapter |
Auto-normalise records to current schema (add defaults, apply renames, drop removed fields) |
TumblingWindow(size, fn) |
Collect N records → emit one aggregate |
SlidingWindow(size, step, fn) |
Overlapping windows; emit every step records |
| In-memory | HTTP | Kafka | |
|---|---|---|---|
| Source | source.NewMemory |
source.NewHTTP |
source.NewKafka |
| Sink | sink.NewMemory |
sink.NewHTTP |
sink.NewKafka |
The AI Debugger collects a snapshot of pipeline metrics (throughput, latency p50/p99, queue depth, error count per stage) and asks Claude to identify bottlenecks and suggest concrete fixes — including a Go config snippet.
dbg := ai.New("", "") // reads ANTHROPIC_API_KEY from env
explanation, err := dbg.Explain(ctx, p.Snapshot(), p.Graph())Or via the Web UI: click "Ask Claude" in the AI Debugger panel.
The embedded Web UI has two views, both served by the binary (no build step, no npm — a single embedded HTML/JS/CSS bundle).
Monitor updates in real time via Server-Sent Events:
- Pipeline graph — live topology with queue depth badges and health colouring
- Stage cards — throughput sparklines, p50/p99 latency, error counts (click a card for an advanced per-stage metrics drawer with pipeline totals + uptime)
- Schema Evolution timeline — version history with field diffs
- AI Debugger panel — on-demand Claude analysis
- Auto-reconnecting SSE, idle/empty states
Builder is a drag-and-drop DAG editor backed by a control plane:
go run ./cmd/drift serve --jobs-dir ./jobs # → http://localhost:8080 (Builder tab)- Drag source / operator / sink blocks onto the canvas, wire operators, configure params
- Save → YAML in the jobs folder, and load a YAML back into the canvas (round-trip)
- Run / Stop pipelines from the UI; the Monitor follows the running pipeline live
- Built on
job.Catalog()so the palette always matches the engine's built-ins - Set
DRIFT_AUTH_TOKENto require a bearer token on the API
pkg/core — Record, Schema, Operator, Source, Sink, Flusher interfaces
pkg/schema — SchemaRegistry (linear versioning, live subscriber notifications)
pkg/operator — Map, Filter, FlatMap, SchemaAdapter, TumblingWindow, SlidingWindow
pkg/pipeline — DAG executor with automatic metrics instrumentation
pkg/source — Memory, Generator, HTTP, Kafka
pkg/sink — Memory, HTTP, Kafka
pkg/metrics — StageMetrics (latency ring buffer, throughput window, queue depth)
pkg/ai — AIDebugger (Claude API integration)
pkg/lineage — Record-level provenance tracker
pkg/wal — Write-ahead log + exactly-once coordinator
pkg/job — Declarative YAML jobs + operator/source/sink catalog
pkg/runner — Control plane: job store (YAML folder) + pipeline runner
pkg/web — Embedded Web UI (monitor + builder) + SSE/control-plane API
cmd/demo — Demo: payment pipeline with live schema evolution
cmd/drift — CLI: run / validate / graph / list / serve
specs/ — Component specs (read before implementing)
skills/ — Claude Code workflow templates
go test ./... # full suite (race detector in CI)
go test -bench=. ./tests/bench/ # benchmarks
go run ./cmd/demo # run demo locallyBefore adding a component: write a spec in specs/, then implement. See skills/add-operator.md.
Throughput baselines (Apple M3, raw operator Process):
| Workload | Records/sec |
|---|---|
| Filter (0 allocs/batch) | ~20M |
| Map | ~6M |
| Map+Filter pipeline | ~2.4M |
| TumblingWindow pipeline | ~7M |
Nexmark vs Flink: Drift implements the full Nexmark suite (all 23 queries, q0–q22) — stateless, windowed, group-by, top-N, joins, and a file sink. A same-machine run (Flink 1.18 in Docker on the same laptop) shows the two are comparable per core on stateless queries (Drift ~1.0–1.5×); Flink scales better across cores on one box for a single query. Drift's edge is operational (single binary, no JVM/cluster/shuffle), not raw throughput. Full methodology, same-iron tables, and the per-query results: BENCHMARKS.md.
Single-process production-grade path (Path A) — complete:
- True DAG executor (fan-out / fan-in)
- Event time + watermarks
- Session windows
- Persistent state backend (BadgerDB)
- Exactly-once semantics (WAL + idempotent sink)
- Record-level lineage
- CLI + declarative YAML jobs
- Visual builder + control plane (
drift serve)
Beyond Path A:
- Distributed execution (multi-node)
- SQL layer
- More connectors (CDC, object storage)
Releases are automated with GoReleaser. Tagging a version cross-compiles the CLI for macOS/Linux (amd64 + arm64), publishes a GitHub Release with prebuilt archives + checksums, and updates the Homebrew tap.
One-time setup:
- Create a public tap repo
gribovan2005/homebrew-drift(empty is fine). - Create a GitHub Personal Access Token with
reposcope (classic) orcontents: read/writeon the tap (fine-grained), then add it to this repo as an Actions secret namedHOMEBREW_TAP_GITHUB_TOKEN(Settings → Secrets and variables → Actions). The built-inGITHUB_TOKENcan't push to a second repo, hence the separate token.
Cut a release:
git tag v0.1.0
git push origin v0.1.0 # → .github/workflows/release.yml runs GoReleaserAfter the workflow finishes, brew install gribovan2005/drift/drift works.
Dry-run the build locally (no publish) with:
goreleaser release --snapshot --clean --skip=publishApache-2.0 — see LICENSE.