Benzene

One message handler, every transport. Benzene is a hexagonal (ports-and-adapters) architecture for message-driven services: write your logic once, against a topic, and reach it over HTTP, queues, streams, and serverless functions — all at once, on the cloud you already run.

Benzene is defined by a language-neutral specification and implemented as idiomatic ports. Pick your language below — the concepts, wire contracts, and conformance fixtures are the same in every one.

Get started Read the docs View on GitHub
Ports: .NET · Go beta · TypeScript beta · Python beta · MIT

Why Benzene?

Mix transports without the glue

Serverless ties your logic to its trigger — an SNS function can't also take SQS, and putting a queue in front of an HTTP service is bespoke plumbing. A Benzene handler is written against a topic, so the same logic is reachable over HTTP, SQS, SNS, Kafka, and more at the same time. You add or change a transport in the wiring, never in the handler.

See what every service does

Handlers, topics, payloads, and validation rules are introspectable. Benzene generates OpenAPI and AsyncAPI specs and a live service map straight from your code, so your contract and cross-service topology track what actually runs instead of a hand-drawn diagram that drifts.

Test-first, out of the box

Every transport ships a test host and helpers, so you exercise a handler exactly as a queue, a function, or HTTP would invoke it: in memory, in a normal unit test, with no cloud and no emulator. Mock a dependency, send a message, and assert the result.

Cross-cutting concerns, once

Correlation IDs, logging, tracing, validation, retries, and health checks are composable middleware shared across every transport. Write them once instead of scattering them across handlers or re-implementing them per event source.

The core idea

Benzene separates what your service does from how it's invoked. A message handler contains your logic. A transport turns an incoming request into a message and routes it to the matching handler through the middleware pipeline — the same pipeline, whichever transport is on the other end, and however many of them at once.

Your Message Handlers ASP.NET Core AWS Lambda Azure Functions Kafka Cloudflare gRPC / Worker

Get started

The same handler, in the language you build in. .NET is the reference implementation; Go, TypeScript, and Python are early ports of the same spec.

dotnet add package Benzene.AspNet.Core --prerelease

[Message("greet")]
[HttpEndpoint("POST", "/greet")]
public class GreetHandler : IMessageHandler<GreetRequest, GreetResponse>
{
    public Task<IBenzeneResult<GreetResponse>> HandleAsync(GreetRequest message) =>
        Task.FromResult(BenzeneResult.Ok(new GreetResponse { Greeting = $"Hello, {message.Name}!" }));
}

// Wire it onto any host - here ASP.NET Core, in Program.cs:
builder.Services.UsingBenzene(x => x.AddMessageHandlers(typeof(GreetHandler).Assembly));
app.UseBenzene(b => b.UseHttp(http => http.UseMessageHandlers()));

go get github.com/daniellepelley/benzene-go

func greetHandler(ctx context.Context, req GreetRequest) benzene.Result[GreetResponse] {
    if req.Name == "" {
        return benzene.BadRequest[GreetResponse]("name is required")
    }
    return benzene.Ok(GreetResponse{Greeting: "Hello, " + req.Name + "!"})
}

// Register it against a topic, reachable over HTTP (and the wire envelope):
benzene.Register(registry, benzene.NewTopic("greet"),
    benzene.Handler[GreetRequest, GreetResponse](greetHandler))

Early port — the API is still settling. See the repo for the current state.

npm install @benzene/express @benzene/core-message-handlers @benzene/http @benzene/results

@httpEndpoint("POST", "/greet")
@message("greet", { requestType: GreetRequest, responseType: GreetResponse })
export class GreetHandler implements IMessageHandler<GreetRequest, GreetResponse> {
  handleAsync(request: GreetRequest): Promise<IBenzeneResultOf<GreetResponse>> {
    return Promise.resolve(BenzeneResult.ok({ greeting: `Hello, ${request.name}!` }));
  }
}

// Wire it onto any host - here Express, in index.ts:
app.use(benzene((pipeline) => useMessageHandlers(pipeline, GreetHandler)));

Early port — the API is still settling. See the repo for the current state.

pip install benzene-core benzene-http

from benzene.core import BenzeneMessageApplication, Registry, message
from benzene.results import Result

@message("say:hello")
async def hello(request: dict) -> Result:
    return Result.ok({"greeting": f"Hello {request['name']}"})

# Wire it into an application - reachable over HTTP via benzene-http:
app = BenzeneMessageApplication(Registry().add(hello))

Early port — the API is still settling. See the repo for the current state.

And it runs wherever you already are

Cloud portability is the bonus, not the pitch — most teams pick one platform and stay. The point is that Benzene meets whichever one your platform team already chose: the same handlers run unchanged on any of these, so "which host" stays a deployment detail rather than an architecture decision.

AWS Lambda, API Gateway, SQS, SNS, Kafka, EventBridge
Azure Functions, Event Hub, Service Bus
Google Cloud Cloud Functions, Cloud Run
Cloudflare Containers
Kubernetes Any container host, with liveness/readiness health checks
Virtual machines / self-hosted An HTTP host or a long-running worker - consumes Kafka, HTTP, or any custom transport

Try it live

No sign-up, no install — these are the same self-contained dashboard pages your own Benzene services would serve, running here against sample data.

Mesh UI

A service-mesh dashboard over sample health checks, contract drift, and cross-service traffic.

Open the demo →

Spec UI

A Swagger-UI-style browser for a sample Benzene message spec — topics, payloads, and validation rules.

Open the demo →

Built for production, not just prototypes

The quickstart is five minutes; the reason to adopt Benzene is what happens after. Three deeper looks, for whoever is asking the question:

Why Benzene

The case for adopting it — lower cost of change, less lock-in, quality by construction, built to last. Read on →

Architecture

Ports and adapters applied honestly: handlers, transports, one pipeline, and a service that describes itself. See how it fits →

Operations

Observability, health, failure handling, and deployment — what it takes to run it, honestly scoped. Run it in production →

Read the docs →