dicer-go · Project Newsletter · May 2026

Project Newsletter · Issue 1 · May 2026
dicer-go
An idiomatic Go port of Databricks' auto-sharding control plane — MVP complete, ready for evaluation.
Go 1.22+ Kubernetes Distributed Systems Open Source
Nils
v0.1.0 · 2026.05
github.com/nilbot/godicer

Contents

01Executive Summary03
02What Is Dicer, and Why Rewrite It04
03Architecture & Design Decisions05
04The Five-Phase Algorithm06
05MVP Status & Deferred Scope07
06Getting Started08
01 · Executive Summary

Executive Summary

dicer-go is a complete Go rewrite of Databricks' Dicer v1.0.0 — a production-proven auto-sharding control plane that replaces static hash-mod sharding with dynamic slice assignment. The MVP ships ~5,056 lines across 56 Go files, fully implements the five-phase assignment algorithm, and is evaluator-ready today.

5,056
Lines of Go across 56 files
6
Total external imports (stdlib dominates)
11
Test files with end-to-end integration coverage
<200 ns
Target Clerk lookup latency at 10k slices

Key Takeaways

  • The five-phase sharding algorithm that drives a 10× database load reduction in Databricks' Unity Catalog is fully ported and running in Go.
  • HTTP+JSON wire protocol and YAML config eliminate the protoc dependency that made the upstream Scala service expensive to operate in Go shops.
  • Lock-free Clerk lookups via atomic.Pointer[Assignment] and binary search mean key-to-pod routing never contends with assignment updates.
Questions this newsletter answers
  • What does Dicer do, and what does it cost to run the upstream Scala version in a Go-native stack?
  • What were the three most consequential design decisions in the Go rewrite, and why were they made?
  • What is production-ready today, and what is explicitly deferred to the next milestone?
02 · Background

What Is Dicer, and Why Rewrite It

Static sharding breaks under load. Dicer was built at Databricks to solve that — and the Go rewrite makes it operationally viable for teams that never wanted a JVM in their Kubernetes cluster.

The Problem with Static Hash-Mod Sharding

In a hash-mod scheme, a key routes to pod = hash(key) % N. This is simple and fast, but it has three failure modes that compound at scale. First, hot-key skew: a small number of keys can generate a disproportionate fraction of traffic, and there is no mechanism to move them to a less-loaded pod. Second, rolling deploys: when N changes, nearly every key remaps, causing a cache miss storm. Third, pod failures: a dead pod takes its entire slice of the keyspace with it, and the recovery path requires either a cold restart or manual re-seeding.

Databricks built Dicer to replace hash-mod with a dynamic slice assignment model. The keyspace is divided into contiguous half-open ranges called slices — each covering a segment of [Empty, Inf) — and pods register for slices by sending heartbeats to a central Assigner. The Assigner runs a load-balancing algorithm every two seconds and publishes a new assignment when the current distribution is outside tolerance. Pods receive assignments via a server-streaming watch endpoint and begin routing accordingly within milliseconds.

In Databricks' production deployment, Dicer reduced database load in Unity Catalog by 10×, improved SQL Query Orchestration from two nines to four nines availability, and delivered cache hit rates above 90% during rolling updates — results that static sharding cannot achieve by design.

Why Rewrite Rather Than Wrap

The upstream Dicer is written in Scala and speaks gRPC with protobuf. Running it in a Go-native infrastructure stack requires a JVM process, a protoc code-generation step for every client, and a gRPC stub in every service that wants to act as a Slicelet. For shops already running Go everywhere, that operational surface is larger than the problem it solves.

The Go rewrite targets the same algorithmic semantics as Dicer v1.0.0 — released 2026-01-13 — while changing three things: the runtime (JVM → Go), the wire protocol (gRPC/protobuf → HTTP/JSON), and the configuration format (textproto → YAML). The routing semantics, the five-phase algorithm structure, and the imbalance tolerance model are preserved exactly.

03 · Architecture & Design Decisions

Architecture & Design Decisions

Three roles, three binaries, one HTTP mux. The design minimizes moving parts so a single-replica Assigner on a Kubernetes cluster is the only infrastructure addition a team needs.

The Three-Role Model

Assigner control plane · :24500 /watch /heartbeat /load Slicelet server-side library heartbeat · load report Clerk client-side library O(log n) lookup · watch heartbeat / load assignment assignment watch (fallback) Solid arrows: primary paths. Dashed: Clerk falls back to Assigner when no Slicelet is available.
Fig 1 — The three-role model. Slicelets register health and load; the Assigner rebalances and publishes; Clerks route keys in O(log n) without locking.

Five Design Decisions Worth Explaining

1. HTTP+JSON over gRPC/protobuf

The upstream wire protocol is gRPC with protobuf — sensible for Scala where grpc-java is a first-class dependency. In Go, adding gRPC means protoc code generation, a google.golang.org/grpc import tree, and a separate listener port. Go 1.22's enhanced net/http mux handles GET /v1/dicer/{target}/watch with path parameters natively. The HTTP+JSON approach eliminates all of that: the Assigner and any NDJSON-capable HTTP client can speak the wire protocol today, including curl.

2. YAML configuration over textproto

Upstream config is textproto, which requires a .proto schema and protoc to parse. YAML has no compile step, ships with gopkg.in/yaml.v3 (the only non-stdlib dependency in the control plane), and is already the dominant config format on Kubernetes. The schema maps directly to the upstream TargetConfigP structure, including scope-based overrides driven by the LOCATION environment variable.

3. Immutable assignments and atomic.Pointer

An Assignment is constructed once, validated on creation, and never mutated. Clerks hold an atomic.Pointer[Assignment] and swap it when a new assignment arrives from the watch stream. Key lookup is a binary search over a sorted slice of entries — no mutex, no channel, no allocation on the hot path. The target for SquidForKey is under 200 ns at 10,000 slices on a modern CPU.

4. In-memory Prometheus without client_golang

The metrics package implements a thin counter/gauge interface backed by a sync/atomic integer store, with a text-format exporter on /metrics. This avoids pulling in prometheus/client_golang — a substantial import tree — while keeping the endpoint compatible with any Prometheus scraper. Teams who want the full registry can swap the backend for promhttp.HandlerFor() in one function call.

5. FNV-1a + SplitMix64 for key hashing

The upstream uses FarmHash, which requires a C extension in Go. dicer-go uses FNV-1a (stdlib) fed through a SplitMix64 avalanche step, producing uniform distribution without a CGO dependency. The resulting hash space is byte-order compatible with Go clients but not with the upstream Scala Clerks — an acceptable trade-off for new deployments. Teams needing bytewise compatibility can substitute FarmHash64 in pkg/dicer/fingerprint.go without touching the algorithm.

04 · The Five-Phase Algorithm

The Five-Phase Algorithm

The Assigner runs one generator goroutine per target. Every two seconds it samples pod health and load, runs five deterministic phases, and either commits a new assignment to the store or skips the tick if nothing changed.

Phase Walkthrough

Phase 1 — Constraint

Each pod declares a replica target: how many slices it wants to serve simultaneously. The constraint phase clamps replica counts to [MinReplicas, MaxReplicas] per the target config. If a pod currently holds more replicas than its maximum, the excess are scheduled for removal from the most-loaded pods first. If fewer replicas exist than the configured minimum, the coldest available pods receive new ones.

Phase 2 — Deallocate

Pods that have not sent a heartbeat within the TTL window are considered unhealthy. All their slice assignments are orphaned and flagged for redistribution in Phase 4. This is the primary recovery path for pod failures during rolling deploys.

Phase 3 — Merge

Adjacent slice pairs whose combined load falls below the merge threshold are coalesced into a single slice. The merge phase runs until the total replica count across all pods is at or below NumResources × MaxAvgSliceReplicas. This prevents the keyspace from fragmenting into thousands of micro-slices during sustained low-traffic periods.

Phase 4 — Place

Orphaned slices from Phase 2 are assigned to healthy pods with available capacity. Placement prefers the pod that previously held the slice — reducing cache miss storms during recovery — and breaks ties by choosing the least-loaded pod. This affinity logic mirrors the upstream "minimal churn" property.

Phase 5 — Split

Slices whose measured load exceeds max_load_hint are split recursively until each half-open range [Low, Mid) and [Mid, High) is within tolerance. The splitter hooks for hot-key isolation (routing a single hot key to its own slice) are present in the code but deferred to a later milestone, matching the upstream's own deferred status for this feature.

The three imbalance tolerance bands — DEFAULT (~10% of max_load_hint), TIGHT (~2.5%), and LOOSE (~40%) — let teams tune how aggressively the Assigner responds to transient load spikes. Tight is suited to SLO-critical caches; Loose is suited to batch pipelines where churn cost exceeds imbalance cost.

Concurrency Model

The generator for each target runs in a single goroutine, ticking on a time.Ticker. This mirrors the upstream SequentialExecutionContext from Scala and ensures the algorithm sees a consistent snapshot of pod health and load within a single tick. The store uses optimistic concurrency control: a write succeeds only if the predecessor generation matches what is currently stored. Watch subscribers receive only the latest assignment — a one-item buffer with overwrite semantics prevents slow consumers from accumulating stale events.

05 · MVP Status

MVP Status & Deferred Scope

The v0.1.0 tag represents a complete, evaluable implementation of Dicer's core control loop. Six features are explicitly deferred — all with a clear rationale and a bounded scope for follow-up.

What's Shipped

Component Status Notes
Core library API (Assignment, Slice, Clerk, Slicelet) Done Full interface, validated on construction
Assigner control plane (HTTP, watch, heartbeat, load) Done Runs on :24500; metrics on :7777
Five-phase assignment algorithm Done Structure correct; tuning constants are best-effort port
In-memory store with OCC and watch fan-out Done Coalescing subscriber buffers, immutable snapshots
Clerk: lock-free lookup + watch consumer Done atomic.Pointer, O(log n) binary search
Slicelet: heartbeat + async load accumulation Done Buffered channel; never blocks request handlers
YAML configuration with scope-based overrides Done Maps to upstream TargetConfigP schema
Prometheus metrics (9 counters/gauges) Done Text-format exporter; no client_golang dependency
Demo CLI triple (assigner, server, client) Done Three-terminal quick-start works end-to-end
Helm chart (single-replica Assigner) Done Deployment + Service + ConfigMap
CI (Go 1.22 & 1.23, gofmt, vet, race test) Done GitHub Actions matrix
Etcd backing store (HA multi-replica Assigner) Stub Returns ErrUnsupported; leader election deferred

Deferred Features and Why

The following are deliberately out of scope for v0.1.0. Each is bounded and has a clear follow-up path.

  • Algorithm constant transcription. The five-phase structure is correct, but the imbalance thresholds and reservation fractions are a best-effort mechanical port of ~603 lines of Algorithm.scala. A line-by-line transcription is the highest-priority follow-up before any production deployment.
  • Etcd backing store and HA. Multi-replica Assigner requires leader election via go.etcd.io/etcd/client/v3/concurrency.Election. The stub is in place; the implementation is scoped to a dedicated milestone.
  • Hot-key isolation. The splitter has hooks for routing a single hot key to its own slice. This mirrors the upstream's own deferred status for this feature.
  • State transfer. Upstream-deferred; not included here.
  • TLS/mTLS. Flag surface matches upstream; certificate handling is deferred.
  • FarmHash compatibility. Teams needing bytewise key-space compatibility with upstream Scala Clerks can substitute FarmHash64 in pkg/dicer/fingerprint.go.
06 · Getting Started

Getting Started

The three-terminal quick-start below runs a fully functional auto-sharding demo in under two minutes. No external dependencies beyond Go 1.22+ and a POSIX shell.

Build

git clone https://github.com/nilbot/godicer
cd godicer
make build          # produces bin/dicer-assigner, bin/dicer-demo-server, bin/dicer-demo-client

Terminal 1 — Start the Assigner

mkdir -p /tmp/dicer/target_config
cat > /tmp/dicer/target_config/demo.yaml <<EOF
owner_team_name: caching
default_config:
  primary_rate_metric_config:
    max_load_hint: 30000
    imbalance_tolerance_hint: DEFAULT
EOF

./bin/dicer-assigner \
  --target-config-dir=/tmp/dicer/target_config \
  --rpc-port=24500 \
  --info-port=7777 \
  --location=kubernetes-cluster:dev/local/public/local/dev/01

Terminal 2 — Start a Slicelet-backed Demo Server

POD_IP=127.0.0.1 POD_UID=$(uuidgen) \
LOCATION=kubernetes-cluster:dev/local/public/local/dev/01 \
./bin/dicer-demo-server \
  --target=demo \
  --assigner=localhost:24500 \
  --rpc-port=24510

Terminal 3 — Send Load via a Clerk-backed Demo Client

./bin/dicer-demo-client \
  --target=demo \
  --slicelet=localhost:24510 \
  --assigner=localhost:24500

The client routes keys through the Clerk, which watches the Slicelet for the current assignment and falls back to the Assigner directly. After a few ticks, the Assigner will emit rebalanced assignments visible in the Terminal 1 log.

Kubernetes Deployment

helm install dicer-assigner deploy/helm/dicer-assigner/ \
  --set image.tag=v0.1.0 \
  --set-string targetConfigs.myservice="owner_team_name: my-team\ndefault_config:\n  primary_rate_metric_config:\n    max_load_hint: 60000\n    imbalance_tolerance_hint: DEFAULT"

Observability

The Assigner exposes Prometheus metrics on :7777/metrics and a health endpoint on :7777/healthz. The nine metrics cover generation count, slice churn (keys added, removed, moved), active target count, and Clerk lookup hit/miss rates. Wire the /metrics endpoint into any standard Prometheus scrape config.

Next Steps
  • Run the three-terminal demo and observe live rebalancing by killing and restarting dicer-demo-server.
  • Review internal/algorithm/constants.go against Algorithm.scala for the algorithm constant transcription milestone.
  • Open an issue or PR on GitHub if you find a divergence from upstream sharding semantics.

References

  • Databricks Dicer v1.0.0 — github.com/databricks/dicer (released 2026-01-13)
  • dicer-go repository — github.com/nilbot/godicer
  • Go 1.22 enhanced HTTP mux — pkg.go.dev/net/http
  • gopkg.in/yaml.v3 — YAML configuration parsing
w