Perspective

One install, four languages, one API: the Aeron Cache developer experience

One brew install, a scriptable CLI, and one identical API across Java, TypeScript, Python and Rust — the Aeron Cache developer experience.

The Aeron Cache team · October 13, 2026

Infrastructure usually makes you earn the first ten minutes

There is an unspoken tax on serious infrastructure: the harder it works, the worse it is to try. The systems that give you predictable tail latency and real high availability tend to greet you with a cluster to stand up, a config matrix to decode, and a client library that assumes you already know the wire protocol. The reward is worth it, eventually — but “eventually” is where a lot of good tools lose people who would have loved them.

We did not want Aeron Cache to be that tool. The engine underneath is uncompromising — a RAFT-replicated deterministic state machine on Aeron Cluster, an SBE binary gateway, patch-native streaming (the deep dives make the case). But the experience of picking it up should cost you an afternoon at most, and the path from your laptop to a production cluster should not require rewriting a single line. This post is about that second half: the developer experience we built on top of the hard parts.

One command to a running cache

The whole system collapses into a single process for local work. The monolith bundles a single-node cluster, the HTTP / WebSocket / SSE gateways, and a web UI — everything the clustered deployment runs, minus the cluster ceremony — behind one Homebrew formula:

brew tap bhf/aeron-cache
brew install aeron-cache
aeron-cache

That third line is the whole setup. You get the banner, a UI at localhost:3000, the REST API on :7070, and the WebSocket stream on :7071 — no YAML, no media driver to wire up, no quorum to reason about:

✅ Aeron Cache is running!
   UI      : http://localhost:3000
   Backend : PID 6221
   Config  : /home/user1/.aeron-cache

The important part is what this is not: it is not a stripped-down demo mode with a different API you’ll outgrow. The monolith speaks the exact same command surface — caches, counters, per-item TTL, JSON merge patch, bulk operations, streaming subscriptions — that the RAFT cluster speaks. When you want more, the gateways you’ve been using are toggled with environment variables in ~/.aeron-cache/backend.env (WEBSOCKET_GATEWAY_ENABLED, SSE_GATEWAY_ENABLED, AERON_GATEWAY_ENABLED), and the clustered backend is a Helm install away (see scaling the gateways). Nothing you learned on the laptop becomes wrong. Start at Getting Started and you’re writing keys in minutes.

A CLI for when you don’t want to write code

Sometimes the fastest client is no client at all. When you’re debugging state, scripting a CI step, or just poking at a cache from a shell, the Aeron Cache CLI — a thin, fast Rust client over the REST API — exposes the full cache and counter surface as plain one-liners:

brew tap bhf/aeron-cache-cli
brew install aeron-cache-cli

aeron-cache create orders
aeron-cache insert orders order:1001 "shipped"
aeron-cache get orders order:1001          # -> order:1001 = shipped
aeron-cache list-caches
aeron-cache stats

Every command is a one-liner over /api/v1, which makes the CLI genuinely scriptable — drop it in a Makefile, a CI job, or a smoke test without a second thought. And because it’s just a REST client, it points anywhere: the local monolith by default, or any backend via --api-url or the AERON_CACHE_API_URL environment variable.

# Same binary, different target — your staging cluster, for instance.
export AERON_CACHE_API_URL=http://staging:7070/api/v1
aeron-cache stats

Counters, TTLs, and cancellable removals follow the same rhythm (increment-counter, insert-timed, cancel-removal); --help on any command prints its signature. The full command reference is one page.

Four languages, one API

Most of the time you do want a client in your app — and here is the part we worked hardest to get right. The embedded clients exist for Java, TypeScript, Python, and Rust, and they are not four independently-drifting SDKs with four personalities. They present the same high-level API, method for method, so a team that works across languages learns the model once:

// Java
var client = new AeronCacheClient("http://localhost:7070", "ws://localhost:7071");
client.createCache("sample-cache");
EmbeddedAeronCache cache = client.getCache("sample-cache");
cache.subscribe(event -> {}, true);          // hydrate a live local mirror
cache.put("stay", "tuned");
System.out.println(cache.getLocal("stay"));  // local read — no network round-trip
# Python — the same shape, the same names
client = AeronCacheClient("http://localhost:7070", "ws://localhost:7071")
client.create_cache("sample-cache")
cache = EmbeddedAeronCache(client, "sample-cache")
await cache.subscribe(lambda e: None, hydrate=True)
cache.put("stay", "tuned")
print(cache.get_local("stay"))               # local read — no network round-trip

That getLocal / get_local is the ergonomic payoff. Every embedded client keeps a local mirror in sync over the stream, so reads are in-process map lookups while writes still flow through the cluster — you get a networked, highly-available store that reads like an in-process map (the mechanism is in streaming subscriptions). The same uniform surface carries counters, JSON merge patch, bulk operations, and a bidirectional WebSocket — and for Java and Rust, the native Aeron SBE gateway — all behind identical calls (the full tour is on embedded clients). Installation is idiomatic per ecosystem: Gradle from GitHub Packages, npm install @bhf/aeron-cache-embedded-client, pip install from git, or a Cargo git dependency.

A small thing that quietly removes a whole category of error handling: every response carries an operationStatus business status — SUCCESS, UNKNOWN_CACHE, CACHE_EXISTS, UNKNOWN_KEY — rather than smuggling business outcomes into HTTP codes and exceptions. “Cache already exists” is a value you branch on, not an exception you catch. The same discipline shows up everywhere: the model is consistent, so your code is too.

The same surface, all the way up

The through-line connecting all of this is a deliberate design choice: there is one command vocabulary, and everything speaks it. The CLI, the four embedded clients, the REST and WebSocket and SSE and Aeron interfaces, the laptop monolith, and the production RAFT cluster are all views onto the same model. The transport is a latency decision, not an API decision — swap HTTP+WebSocket for the bidi WebSocket or the native Aeron gateway and your calls don’t change (see Aeron + SBE transport).

That is what makes the easy on-ramp honest rather than a trap. The thing you prototyped against brew install aeron-cache on Monday is, byte-for-byte at the API, the thing you deploy to a 3-node cluster later — the determinism that makes the single node correct is exactly what makes the cluster consistent (see RAFT consensus). You don’t graduate from the beginner tool to the real one. There is only one tool, and it meets you wherever you start.

Takeaway: good infrastructure shouldn’t make you prove you’re worthy before it does anything useful. One brew install gives you a full cache and UI; one Rust CLI scripts it from any shell; four embedded clients give you the identical API — and local-mirror reads — in Java, TypeScript, Python, and Rust; and none of it has to change when you scale from your laptop to a RAFT cluster. Try it in an afternoon at Getting Started, then see where it lands against the alternatives in comparison.