blob: e37bb500992b9e2c5d6128ccc9af82947690c91b [file] [view]
# Apache EventMesh Architecture
> **Audience:** anyone who wants the structural view — the three planes,
> module decomposition, and the publish wire path. Code locations are
> relative to the module root.
---
Apache EventMesh is a **stateless application-layer event mesh** built around three
separation-of-concerns planes:
1. **Control plane** — subscription / session / state management, cluster coordination,
routing decisions. Stored in a *pluggable meta store* (Nacos today; in-memory single-instance)
and *pluggable storage SPI* (RocksDB / MQ-as-WAL / external KV).
2. **Data plane** — the wire path from a client SDK to a backend MQ, then to subscriber
delivery transports (HTTP long-polling, SSE, WebSocket, request-reply). The
EventMesh Runtime is the only place where delivery semantics live.
3. **Agent plane** — the A2A (Agent-to-Agent) gateway that maps synchronous
tool-call / JSON-RPC semantics onto the same MQ-backed pub/sub substrate, so
multi-agent workloads get durability, observability, and quota from the same
pipeline as ordinary events.
This page gives the **structural** view. For end-to-end data flow diagrams see
`docs/architecture/redesign.md`. For the security / quota gate, see
the "Security gate" section below and the `#5304` reference docs; for the storage
capability contract, see the `eventmesh-storage-api` TCK.
---
## 1. Top-level decomposition
```
┌──────────────────────────────────────────────────────────────────────┐
│ EventMesh Repository │
├──────────────────────────────────────────────────────────────────────┤
│ eventmesh-runtime // the only deployable that ingests │
│ // and dispatches events │
│ eventmesh-storage-plugin/ // pluggable MeshStoragePlugin SPI │
│ eventmesh-storage-api // interface + TCK (StorageCapabilities, │
│ │ // LiteTopicCapable, MeshStoragePlugin) │
│ eventmesh-storage-* // backends: RocketMQ 4.x/5.x, Kafka │
│ // (SPI: add more via MeshStoragePlugin)│
│ eventmesh-connector-plugin/ // 24+ source/sink connector plugins │
│ eventmesh-connector-runtime // standalone process running them │
│ eventmesh-protocol-plugin/ // eventmesh-protocol-api + cloudevents│
│ // + meshmessage + grpc + a2a │
│ eventmesh-spi // ExtensionFactory, EventMeshSPI, │
│ // EventMeshExtensionType │
│ eventmesh-common // shared util / config / metrics │
│ eventmesh-sdks // client SDKs (Java, …) │
│ eventmesh-examples // runnable end-to-end examples │
│ eventmesh-architecture-guard // ArchUnit rules + CI workflow │
│ eventmesh-agent // reference A2A client / agent core │
└──────────────────────────────────────────────────────────────────────┘
```
* **One deployable process for the data plane**: `eventmesh-runtime`. It is
*stateless with respect to delivery* (cluster state lives in the meta store and
storage SPI), which means horizontal scale-out is a config change.
* **Storage is a contract, not a driver**: every backend — including the new
RocksDB-local path — implements `MeshStoragePlugin` and passes
`MeshStoragePluginTCK`. The runtime only knows the SPI; it does not import any
vendor JAR.
* **Connectors run in a separate process**: `eventmesh-connector-runtime` (admin
HTTP server + per-connector thread pool). They connect to the runtime over
HTTP+CloudEvents, not in-process, so a misbehaving connector cannot poison the
data plane.
* **Architecture is enforced, not aspirational**: `eventmesh-architecture-guard`
is an ArchUnit test that fails CI if any module violates the layered rules
(e.g. `eventmesh-common` may not depend on `eventmesh-runtime`). See
`#5305` for the FAIL-mode workflow.
---
## 2. Runtime data plane — the wire path
A single publish, end to end:
```
client SDK EventMesh Runtime storage
───────── ───────────────── ───────
CloudEventsClient.publish(ce) ┌─ UniHttpServer (Netty HTTP/S) OffsetStore
│ POST /events/publish ───────────▶ │ • TlsContextFactory (local KV)
│ │ • RequestContext (security gate) ─┘
│ │ • SecurityGate.check(ctx, frame) ───┐
│ │ ├─ FilterChain (auth / acl) │ ACL: AclFilter,
│ │ │ (TokenAuth, Signature, │ TokenAuthFilter,
│ │ │ AclFilter) │ SignatureVerifierFilter
│ │ ├─ QuotaManager (per-tenant) │ Quota:
│ │ └─ AuditSink │ TenantQuotaManager
│ ▼ │
│ UniIngressService.publish │
│ │ │
│ │ EventMeshFrame (single protocol) │
│ ▼ │
│ FilterChain.invoke(frame) │
│ ▼ │
│ Producer.send(StorageResourceService) ─▶│ ──▶ MQ-as-WAL
│ │ │
│ │ StorageClient.append(...) │
│ ▼ │
│ SendCallback │
│ ▼ │
◀──── HTTP 200 / 4xx (rejected by gate) │ AuditSink.emit(...) ──▶ metrics │
└─────────────────────────────────────┘
```
A subscribe path mirrors this in reverse. A subscriber's `POST /events/subscribe`
registers a `Subscription` in `ClusterSubscriptionStore` (L2). The runtime's
`LocalDeliverer` polls the storage offset, applies the subscription filter, then
pushes the CloudEvent over the chosen transport (long-poll / SSE / WebSocket /
request-reply).
The legacy TCP / gRPC / OpenMessaging SDKs still work because the new
`EventMeshFrame` adaptor (`eventmesh-runtime/.../protocol/meshmessage`) and the
`UniTcpServer` (public/internal split per `#5297`) preserve the wire format and
semantics of the old `MeshMessage` / `OpenMessage` clients; the legacy gRPC
SDK family is served by the opt-in `EventMeshGrpcServer` bridge (#5411).
### Key classes
| Component | File | Role |
| --- | --- | --- |
| HTTP entry | `eventmesh-runtime/.../http/UniHttpServer.java` | Netty HTTP/S; entry of all `/events/*` + `/a2a/*` traffic; `withSecurityGate(...)` wiring point |
| WebSocket entry | `eventmesh-runtime/.../http/UniWsServer.java` | WebSocket transport for subscribers |
| Legacy gRPC bridge | `eventmesh-runtime/.../grpc/EventMeshGrpcServer.java` | Opt-in port-10205 bridge serving the 1.x `PublisherService`/`ConsumerService`/`HeartbeatService` (#5411) |
| Ingress orchestrator | `eventmesh-runtime/.../ingress/UniIngressService.java` | Frame-typed facade; single protocol path (`EventMeshFrame`) used by both HTTP and the legacy TCP adaptor |
| Security gate | `eventmesh-runtime/.../security/gate/SecurityGate.java` | Opt-in unified gate (see §4) |
| Filter chain | `eventmesh-runtime/.../security/FilterChain.java` | Auth + ACL filters executed before the gate |
| A2A HTTP handler | `eventmesh-runtime/.../a2a/A2AGatewayHttpHandler.java` | Netty handler for `/a2a/*`; `withSecurityGate(...)` wiring point |
| Connector scheduler | `eventmesh-runtime/.../connector/ConnectorScheduler.java` | Validates `ConnectorDef` against the gate; `withSecurityGate(...)` wiring point |
| Storage SPI | `eventmesh-storage-plugin/eventmesh-storage-api/.../storage/MeshStoragePlugin.java` | Capability-aware contract; backends: `eventmesh-storage-rocketmq`, `eventmesh-storage-rocketmq5`, `eventmesh-storage-kafka` |
| Offset store | `eventmesh-runtime/.../offset/` | Local offset tracking; survives restart via RocksDB or via the meta store |
| Delivery state | `eventmesh-runtime/.../state/` | `DeliveryStateStore` (RocksDB) for at-least-once, dead-letter handling |
---
## 3. Control plane — state, sessions, and coordination
The runtime keeps no delivery state in memory. Every piece of cluster-visible
state lives behind one of the storage SPI interfaces, each with a *clear level*
classification:
| Level | Purpose | Examples | Stores |
| --- | --- | --- | --- |
| **L1 — local-only** | per-instance delivery hints, cheap to recompute | `OffsetStore` (RocksDB or local KV) | Local file / memory |
| **L2 — cluster-shared** | subscriptions, sessions, agent cards | `SubscriptionStore`, `SessionStore`, `AgentCardRegistry` | Meta store (Nacos; Consul/ETCD/ZK are SPI work) |
| **L3 — durable-egress** | dead-letter, task records | `DeadLetterStore`, `TaskStore` | Meta store with CAS + fencing epoch |
The contract for each interface is the same:
* a `MeshStoragePlugin` implementation in `eventmesh-storage-plugin/*`,
* a thin runtime-side adapter (e.g. `ClusterSubscriptionStore`,
`SessionRegistry`) that adds **fencing tokens / epochs** on top of the meta
store, so split-brain does not corrupt state,
* a TCK (`MeshStoragePluginTCK`) that every backend must pass.
State changes propagate through a **prefix-watch** pattern on the meta store
(see `eventmesh-runtime/.../cluster/DynamicConfigWatcher.java`). A
`SubscriptionStore.subscribe(...)` call writes to the meta store; every runtime
instance receives the change and re-evaluates its local delivery set. That keeps
the cluster eventually consistent without inventing a Raft implementation.
For partition-owned delivery, the runtime implements the
`PARTITION_OWNED_PULL` topology (`#5309`): each topic partition is owned by
exactly one runtime, the owner drives the offset, and ownership transfers
are epoch-protected. The owner drives the offset because the storage SPI
itself does not know what is consumed.
---
## 4. Security gate (issue #5304)
Until 2026 the runtime had no consistent multi-tenant gate. ACL was per-filter
and easy to miss, rate limits were topic-only, and there was no audit trail.
Issue **#5304** added an **opt-in unified gate** that you can wire to the three
ingress points (HTTP, A2A HTTP, Connector admin).
```
ingress request
│
▼
SecurityGate.check(RequestContext, EventMeshFrame)
│
├─ 1. FilterChain.invoke(frame) ── TokenAuthFilter → SignatureVerifierFilter → AclFilter
│ produces verdict (ALLOW / DENY)
│
├─ 2. if ALLOW → QuotaManager.acquire(ctx, Resource)
│ resources: CONNECTIONS | SUBSCRIPTIONS | THROUGHPUT | BACKLOG
│ per-tenant counter; default = UnlimitedQuotaManager (no enforcement)
│
└─ 3. AuditSink.emit(decision, ctx, frame) ── LoggingAuditSink (default) or custom sink
metrics already cover the aggregation view
```
Key design points:
* **Opt-in, not opt-out.** A `null` gate is a legacy behaviour. New deployments
are expected to install a `TenantQuotaManager` and a custom `AuditSink`.
* **One `RequestContext` per request.** It is immutable, builder-built, and
carries `tenantId`, `principal`, `roles`, `scopes`, `credential`,
`remoteAddress`, `source`, `traceContext`, `quotaKey`, and an `Operation`
enum (`PUBLISH` / `SUBSCRIBE` / `ACK` / `CONNECTOR` / `A2A` / `ADMIN`).
* **`FilterChain` is reused** for the actual auth/ACL verdict, so the gate does
not duplicate policy; it composes it. New filters drop in without touching
the gate.
* **Quota is keyed by `RequestContext.quotaKey()`** (defaults to `tenantId`),
and uses the `Resource` enum so a backend can add custom resources via
extension.
### Wiring
* `UniHttpServer#withSecurityGate(SecurityGate)` — gates every `/events/*`
endpoint, including the SSE and WebSocket upgrades.
* `A2AGatewayHttpHandler#withSecurityGate(SecurityGate)` — gates every A2A
endpoint; the A2A operation is recorded in the context so the quota can
distinguish a `tasks/send` from a `tasks/get`.
* `ConnectorScheduler#withSecurityGate(SecurityGate)` — gates
`createConnector(...)` and raises `ConnectorAccessDeniedException` (which
the admin server maps to HTTP 403).
The **10 unit tests** in `SecurityGateTest` cover each Resource, both
allow/deny verdicts, every Operation, and a smoke test that the gate
short-circuits before the downstream SPI is touched.
---
## 5. Agent plane — A2A on the same substrate
The A2A (Agent-to-Agent) protocol is an HTTP/JSON contract for invoking a
remote agent as if it were a local function, with a **task** lifecycle
(`submitted → working → completed | failed | canceled`) that maps cleanly onto
a durable `TaskStore`. EventMesh implements the gateway side on top of the
data plane:
```
agent A (or client) EventMesh Runtime
──────────────── ─────────────────
POST /a2a/tasks/send ───────────────▶ A2AGatewayHttpHandler
│
│ SecurityGate.check(ctx, frame) (Operation.A2A)
▼
A2AGatewayService.submit(task)
│
│ TaskStore.create(task, taskEpoch) ─▶ L3 (meta, CAS)
│
│ EventMeshA2ATransport.send(target, task) ─▶ topic
│ topic = A2ATopicFactory.agentInbox(target)
▼
async fan-out to target agent's listener
│
▼
agent B (or its runtime) processes task
│
│ POST /a2a/tasks/{id}/reply ─▶ TaskStore.complete(...)
▼
completion event published on
A2ATopicFactory.gatewayResponseTopic(...)
│
▼
◀─── Agent A subscribes (SSE on /a2a/tasks/{id}/stream) ◀── emit
```
Key classes:
* `A2ATopicFactory` — `agentInbox(agentId)`, `gatewayResponseTopic(ns, gw, taskId)`,
and a `+` wildcard for in-process tests. The convention is the same as
Pulsar / MQTT — **single-level** `+`, **not** globstar `*`.
* `A2AGatewayService` — durable task store facade. Every mutation is
epoch-protected: a stale write with a `taskEpoch` different from the create
value is rejected. This is the only safe way to handle out-of-order
responses from multiple agents.
* `AgentCardRegistry` — declares what an agent can do (`/a2a/agent-card`).
Two implementations: `InMemoryAgentCardRegistry` (default) and a
Meta-backed adapter (D2 of the A2A rollout, see issue `#5302`).
* `EventMeshA2ATransport` — bridges the A2A `MessageTransport` SPI to the
same `UniIngressService` the data plane uses, so reliability, quota, and
observability are uniform.
The protocol is **Experimental** (see capability status table). The wire
contract is the JSON envelope from
[docs/feature/a2a.md](../feature/a2a.md); the runtime
implementation is documented in
[docs/architecture/redesign.md](redesign.md).
---
## 6. Connector plane — source / sink as a separate process
A connector is a long-running process that copies events between EventMesh and
an external system (Kafka topic, Slack channel, MySQL table, S3 bucket, HTTP
endpoint, MCP tool, ChatGPT prompt, …). The `eventmesh-connector-plugin` repo
contains 24 plugin implementations; `eventmesh-connector-runtime` is the
deployable that hosts them.
```
external system EventMesh
────────────── ─────────
source side: ▲
┌──────────┐ poll / listen ┌──────────────────┐ │
│ Kafka │ ───────────────▶ │ Sink side: │ │
│ MySQL │ │ SinkConnector │ │
│ S3 │ │ (CloudEvent in) │ │
│ file │ └────────┬─────────┘ │
└──────────┘ │ │
▼ │
EventMesh HTTP │
/events/publish │
───┘
▲
│
EventMesh HTTP │
/events/subscribe ───┘
│
▼
Source side:
SourceConnector
│
▼
external write
(HTTP / JDBC / gRPC / …)
```
* `ConnectorDef` is the immutable, validated description of one connector:
`id`, `className`, `mode` (`source` / `sink` / `source-sink`),
`topic`, `clientId`, `sinkClass`. New fields are added via the
`ConnectorDef.Builder` — never by setter injection.
* `ConnectorScheduler` owns the lifecycle (`init → start → stop`) and
registers each connector against the SecurityGate. A misconfigured
connector is rejected **before** the JAR is loaded, so a plugin bug
cannot take down the runtime.
* Connectors talk to the runtime over **HTTP + CloudEvents**, never
in-process. They can be deployed, scaled, and restarted independently.
See `eventmesh-connector-runtime/.../ConnectorManager.java` for the
lifecycle and `eventmesh-connector-runtime/.../ConnectorAdminServer.java`
for the admin HTTP surface.
---
## 7. Module boundary rules (architecture-guard)
`eventmesh-architecture-guard` is an ArchUnit test suite that **fails the
build** if a module violates the layering rules. It runs in two places
(issue `#5305`):
* locally on `./gradlew :eventmesh-architecture-guard:check` so a developer
gets a 30-second feedback loop, and
* on every PR via `.github/workflows/architecture-guard.yml` (Ubuntu-only)
with a path filter to skip irrelevant pushes.
The current rule set covers:
* `eventmesh-common` may not depend on `eventmesh-runtime` (and vice versa
for any runtime-only class).
* `eventmesh-storage-api` may not depend on any concrete backend.
* `eventmesh-protocol-plugin/*` is partitioned into `public` (SPI) and
`internal` (implementation) — the `public → internal` direction is
allowed; the reverse is not.
* `eventmesh-connector-plugin/*` plugins may not depend on
`eventmesh-runtime`; they only see the CloudEvents HTTP contract.
* `eventmesh-agent` is allowed to depend on `eventmesh-sdks` and
`eventmesh-protocol-api` only.
If a future change needs a cross-module dependency, the rule must be
**amended in code review**, not bypassed. The 30-second local loop and the
PR workflow are both in place to make this discipline cheap.
---
## 8. Cross-cutting concerns
### Metrics and observability
* Prometheus metrics under `org.apache.eventmesh.metrics.*` cover the
gate (allowed/denied counts per Operation, per Resource), the runtime
(publish/subscribe rate, queue depth), and the connectors (per-connector
throughput, error rate).
* Trace context (`RequestContext.traceContext`) propagates through every
plane so a single `traceId` follows an event from the client SDK to
the storage backend and back out to the subscriber.
* The audit sink is the place to add custom per-tenant audit
destinations (Kafka, S3, …).
### Configuration
* Runtime configuration is documented in
[docs/quickstart/configuration.md](../quickstart/configuration.md). New
keys are added behind the existing prefixes (`eventmesh.runtime.*`,
`eventmesh.security.gate.*`, `eventmesh.storage.*`,
`eventmesh.connector.*`) — never under a global `eventmesh.*` root.
* Per-backend settings are isolated (e.g.
`eventmesh.storage.rocketmq.namesrvAddr` is **not** visible to a
Kafka backend). The runtime boots a backend only if its class is on
the classpath.
### Build & test
* Gradle multi-module; `:eventmesh-runtime:assemble` produces the
runnable distribution.
* `./gradlew :eventmesh-architecture-guard:check` runs the ArchUnit
rules before unit tests.
* Integration tests use Testcontainers (Nacos, RocketMQ) — not
embedded servers — to catch backend-version drift.
---
## 9. Protocols and SDKs
EventMesh supports several wire protocols and ships client SDKs in
multiple languages. The canonical inventory - including GA / Beta /
Experimental / Legacy status for each protocol, the server-side
protocol plugin that handles it, and the corresponding client SDK
surface - lives in [docs/feature/protocols.md](../feature/protocols.md). The policy
summarized there:
* The modern **HTTP + CloudEvents + EventMeshFrame** path is the
only path that downstream code should depend on. It is GA.
* Legacy **TCP** (MeshMessage, OpenMessaging) and the legacy
HTTP codec are still supported for backward compatibility but
are explicitly marked Legacy and receive only critical bug
fixes. See `docs/feature/protocols.md` for the migration path.
* gRPC framing is **Beta**: stable, but the API surface may shift. The
legacy SDK gRPC surface is served again as an opt-in compatibility bridge
on `eventmesh.grpc.port` since #5411 (see `docs/feature/protocols.md` §1.1).
* A2A is **Experimental** until the readiness checks in #5340
pass.
The Java SDK (`eventmesh-sdk-java`) demotes the OpenMessaging API
dependency from `api` to `implementation`, so a modern user who
depends only on `org.apache.eventmesh.client.cloudevents.*` does
not see OMA types on their classpath. The `eventmesh-architecture-guard`
ArchUnit rules + CI workflow verify that the runtime's modern
ingress path does not import legacy wire types.
---
## Documentation
The [documentation map](../index.md) organizes everything by audience. Highlights:
| Page | What it covers |
| --- | --- |
| [Introduction](../introduction.md) | What EventMesh is; the three planes; deployment shape |
| [Getting started](../quickstart/getting-started.md) | Zero-to-running runtime; per-backend quickstarts |
| [Configuration](../quickstart/configuration.md) | Every runtime key, security & quota, per-backend overrides |
| [Java client guide](../feature/client-java.md) | `CloudEventsClient` walkthrough: pub/sub, request-reply, SSE, WebSocket, lite topics |
| [Pub/sub](../feature/pubsub.md) | Topics, distribution modes, filtering |
| [Reliable delivery](../feature/delivery-reliability.md) | ACK tracking, retries, DLQ, crash recovery |
| [A2A protocol](../feature/a2a.md) | A2A wire contract and task lifecycle |
| [Security](security.md) | The unified gate, TLS/mTLS, admin token |
| [HTTP API](../feature/http-api.md) | Traffic endpoints |
| [Admin API](../feature/admin-api.md) | Operational surface + token guard |
| [Protocols & SDKs](../feature/protocols.md) | Wire protocol inventory and per-language SDK status |
| [Storage SPI](../feature/storage-spi.md) | `MeshStoragePlugin` contract and capability matrix |
| [Deployment](../feature/deployment.md) | Modes, Docker, multi-instance, runbooks |
| [Design records](../index.md#design-records-contributors) | Redesign rationale, evidence, plans |