Zilla is a multi-protocol event-driven gateway that bridges HTTP, WebSocket, SSE,
gRPC, MQTT, and MCP to Apache Kafka. It is configured entirely via zilla.yaml,
with no code changes required for protocol mapping. All runtime behavior is
defined by a pipeline of named bindings.
runtime/ # Core engine and all bindings
engine/ # EngineWorker, config loader, stream model
binding-tcp/ # TCP server/client binding
binding-tls/ # TLS binding
binding-http/ # HTTP/1.1 + HTTP/2 binding
binding-kafka/ # Kafka cache + client binding
binding-mqtt/ # MQTT binding
binding-grpc/ # gRPC binding
binding-sse/ # Server-Sent Events binding
binding-http-kafka/ # HTTP↔Kafka proxy binding
binding-mqtt-kafka/ # MQTT↔Kafka proxy binding
binding-grpc-kafka/ # gRPC↔Kafka proxy binding
...
specs/ # Integration test specifications (IT)
incubator/ # Bindings under active development
build/flyweight-maven-plugin/ # Code generator for flyweight types (local peer module, not a remote artifact)
This file holds repo-wide guidance only. When you start working in a subtree,
its AGENTS.md is loaded automatically with more detail:
| Path | Scope |
|---|---|
| runtime/AGENTS.md | Java module system, stream model, binding kinds, EngineWorker threading, factory + flyweight pattern, server/client/proxy patterns, per-stream field naming, buffer slots, decode strategy, unit tests |
| runtime/engine/AGENTS.md | Engine SPI conventions (Javadoc neutrality, null defaults), EngineContext implementer fan-out, test implementations for engine concepts |
| runtime/binding-kafka/AGENTS.md | Kafka local cache (mmap segments, IoUtil.unmap(), retention timestamps) |
| specs/AGENTS.md | .rpt script structure, folder layout, IT method naming, XxxFunctions builders/matchers, JUEL typed variants, k3po + JUnit 4 migration, JSON schema patches, required spec coverage |
Zilla uses Maven with Java 25.
Every new Maven project directory must include mvnw and mvnw.cmd copied
from an existing module — this applies to all new projects regardless of type
(runtime/, specs/, incubator/, etc.).
# Add license headers to new files — run this first after creating new source
# files, otherwise the build will fail on the license check before compilation
./mvnw license:format
# Full build with tests
./mvnw install
# Skip integration tests (faster)
./mvnw install -DskipITs
# Skip all tests
./mvnw install -DskipTests
# Build a specific module
./mvnw install -pl runtime/binding-http -am
# Run a single unit test class (*Test.java, no k3po) — class names are type-prefixed: Http*
./mvnw test -pl runtime/binding-http -Dtest=HttpConfigurationTest*IT.java classes require verify, never test — this is not optional. Every
k3po-based integration test class is named *IT.java and depends on the
k3po:start/k3po:stop goals, which are bound to the pre-integration-test/
post-integration-test lifecycle phases. Those phases only run as part of
verify (or integration-test); the test phase never reaches them. Running
./mvnw test -Dtest=SomeIT will still find and attempt the class (-Dtest
bypasses Surefire's normal *Test.java-only file pattern), but the k3po
control server is never started, so every test fails with a misleading
Failed to connect. Is K3PO ready? — which looks like a test failure but is
actually a wrong-command error. If you see that error, the fix is almost
never in the test or the code — it's the command:
# WRONG for *IT.java — k3po never starts, fails with "Is K3PO ready?"
./mvnw test -pl runtime/binding-http -Dtest=HttpServerIT
# RIGHT — runs the full lifecycle including k3po:start/k3po:stop
./mvnw verify -pl runtime/binding-http -Dit.test=HttpServerIT
# RIGHT — no -Dit.test filter runs every IT in the module
./mvnw verify -pl runtime/binding-httpA crash or hang from test -Dtest=SomeIT (SIGSEGV, "forked VM terminated
without properly saying goodbye", event-assertion mismatches) is a symptom of
this same root cause, not a real bug — before spending time diagnosing a
crash from an *IT run, confirm it was launched with verify.
The Maven plugin in build/flyweight-maven-plugin/ generates flyweight Java
classes from .idl files. Always run a full build after modifying any .idl
file. It is a peer reactor module, not a published remote artifact — it comes
from this same checkout, not from a Maven registry.
Zilla follows a strict test-first discipline. For every new feature or bug fix:
- Write the spec script(s) and/or unit tests first, before any implementation
- Confirm the tests fail for the right reason against the current code
- Implement until the tests pass
- Do not open a PR with implementation code that has no corresponding tests
This is especially important for new bindings. The spec scripts define the correct protocol behavior; the Java implementation exists to satisfy them. Never write implementation code and retrofit tests to match it — that defeats the purpose.
Spec script conventions and unit-test conventions are detailed in specs/AGENTS.md and runtime/AGENTS.md.
Repo-level scaffolding (everything before module-specific implementation):
- Open a GitHub Issue to discuss the design before writing any code
- Create
specs/binding-<n>.spec/and write.rptscripts for the happy path and key error scenarios, derived from the relevant protocol specification — see specs/AGENTS.md - Create
runtime/binding-<n>/andspecs/binding-<n>.spec/following the existing module layout. Every new project directory (bothruntime/andspecs/) must include these top-level files copied from an existing module:COPYRIGHT,LICENSE,NOTICE,NOTICE.template,mvnw,mvnw.cmd. All new components use the Aklivity Community License — copyLICENSE-AklivityCommunity,COPYRIGHT-AklivityCommunity, andNOTICE-AklivityCommunityfrom the top-level repository directory, renaming them toLICENSE,COPYRIGHT, andNOTICE.templaterespectively in the new module. Then generateNOTICEby running./mvnw notice:generate --projects <path/to/project>from the repository root; do not copyNOTICEfrom another module as it must reflect the new module's actual dependencies. Never editNOTICEfiles directly — always regenerate via./mvnw notice:generate --projects <path/to/project>; manual edits will be overwritten. Source file headers must carry the Aklivity Community License copyright notice (Copyright 2021-2024 Aklivity Inc); run./mvnw license:formatto apply the correct header automatically - Add the module to
runtime/pom.xmland the rootpom.xml
Then proceed with the runtime-side implementation steps in
runtime/AGENTS.md and the spec/IT steps in
specs/AGENTS.md. Confirm ./mvnw install passes including
all ITs before opening the PR.
All Java code must pass the project checkstyle rules defined in
conf/src/main/resources/io/aklivity/zilla/conf/checkstyle/configuration.xml.
Run ./mvnw checkstyle:check to verify before committing. Key rules to be
aware of: 4-space indentation (no tabs), 130-character line limit, opening
braces on a new line (LeftCurly option nl), closing braces alone on their
own line (RightCurly option alone), no trailing whitespace, imports ordered
by group (java, javax, jakarta, org, com) with a blank line between
groups and no star imports.
- YAML files use 2-space indentation, no tabs; JSON files use 4-space indentation, no tabs
- Prefer non-block lambdas (expression lambdas) over block lambdas (
{ return ...; }) — even when the expression spans multiple lines via a builder chain, keep it as a single expression without braces or an explicitreturn - Method parameters are each on their own line, indented 4 spaces relative to
the method declaration, with the closing
)on the same line as the last parameter:
private void onNetworkData(
long traceId,
long authorization,
int reserved,
OctetsFW payload)
{- Methods should have a single
returnstatement at the end where possible; avoid early returns except for guard clauses at the very top of a method - Avoid the
...IfNecessarymethod naming suffix (e.g.,doEndIfNecessary,cleanupDecodeSlotIfNecessary) — name methods for what they do (doEnd,cleanupDecodeSlot); internal conditionality based on stream state or slot value is an implementation detail that does not belong in the name - Java 21; no preview features
- No Lombok
- Use
jakarta.jsonAPIs (e.g.,JsonObject,JsonReader,JsonParser) for JSON processing — do not introduce Jackson (com.fasterxml.jackson) - Prefer interface types over implementation classes for field, parameter, and
return types where a suitable interface exists (e.g.,
ListoverArrayList,MapoverHashMap,ConcurrentMapoverConcurrentHashMap) - Never use fully qualified class names as field, parameter, or variable types —
add an
importand use the simple type name. The only exception is a naming collision where two different packages define the same class name; in that case qualify the less-frequently-used type - Package-private classes preferred over public where there is no SPI contract
finalon all fields; immutable config objects- Flyweight field names use the
*RO/*RWsuffix convention consistently - Error paths must call
cleanup()and release any acquired resources before returning - Log via the Zilla event system (
BindingEvent), notjava.util.loggingor SLF4J, on the hot path
Checkstyle enforces a strict import order. Violations cause build failures, so always sort imports alphabetically by fully-qualified package name within each group, and separate groups with a blank line in this order:
java.*javax.*jakarta.*org.*com.*io.*(covers allio.aklivity.zilla.*imports)
Within the io.aklivity.zilla.runtime.engine.* sub-packages the alphabetical
rule means, for example:
import io.aklivity.zilla.runtime.engine.catalog.CatalogHandler; // c
import io.aklivity.zilla.runtime.engine.guard.GuardHandler; // g
import io.aklivity.zilla.runtime.engine.model.ValidatorHandler; // m
import io.aklivity.zilla.runtime.engine.poller.PollerKey; // p ← before store
import io.aklivity.zilla.runtime.engine.store.StoreHandler; // s ← before vault
import io.aklivity.zilla.runtime.engine.vault.VaultHandler; // v
When adding a new import, insert it at the correct alphabetical position — do not append it at the end of the group.
| Dependency | Purpose |
|---|---|
agrona |
Lock-free ring buffers, flyweight buffer access, IoUtil for mmap |
zilla:maven-plugin |
Generates flyweight Java from .idl type definitions |
junit5 |
Unit and integration tests |
mockito |
Mocking in unit tests |
- Fork the repo and create a branch using gitflow naming conventions:
feature/<short-description>for new features, orfix/<issue-number>-<short-description>for bug fixes - Make changes; ensure
./mvnw installpasses with no failures - Open a PR against the
developbranch (notmain) - PRs require at least one approving review from a maintainer
- Commit messages follow Conventional Commits:
feat(binding-http): add trailers supportfix(engine): release mmap'd segment on log rotation - Do not include generated sources (
target/) or IDE files in commits
When a PR build fails, always fetch the actual CI logs before attempting to diagnose or fix the failure. Do not guess at the cause based on the change diff alone — retrieve the logs first using GitHub MCP tools, or ask the user to provide them. Build failures often have non-obvious root causes (e.g., a checkstyle violation, a transitive module-info issue, or a flaky unrelated test) that are impossible to diagnose without the log output.
For significant new bindings or behavior changes, open a GitHub Issue first to discuss the design before writing code.
Many modules in this repo exist to be depended on by other modules — both
inside this repo (common-avro, common-json, common-protobuf, the engine
SPI, any module under runtime/*/ext) and beyond it. When filing an issue or
writing a commit/PR description for a change to one of these, justify it
entirely in terms of that component's own contract or capability gap — never
by naming or describing a consumer that depends on it: not a sibling module,
not a downstream binding, not a commercial extension, not a specific product
or feature.
Test before writing: strip away everyone who currently depends on this component — present and future, in-tree and external. Does the description still stand on its own as "this component's contract had gap Y, now it has capability Z"? If it only makes sense once the reader knows who wanted it, the description is coupling this component to a consumer's specifics, and should be rewritten in terms of the gap itself.
This applies in both directions across the dependency graph: a common-avro
change should never cite model-avro's use case as its motivation, an engine
SPI change should never cite a specific binding's or model extension's use
case, and this repo's own changes should never cite a downstream or
commercial consumer's feature as their reason for existing.
- Docs: https://docs.aklivity.io/zilla/latest/
- Binding reference: https://docs.aklivity.io/zilla/latest/reference/config/bindings/
- How Zilla Works (architecture deep-dive): https://www.aklivity.io/post/how-zilla-works
- Examples: https://github.com/aklivity/zilla-examples