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.
Coming from three separate services today? Why Benzene opens with the before-and-after picture.
Get started
The same handler, in the language you build in.
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()));
// The same handler from HTTP *and* SQS in one AWS Lambda - change the wiring, never the handler:
app.UseAwsLambda(aws => aws.UseApiGateway(api => api.UseMessageHandlers())
.UseSqs(sqs => sqs.UseMessageHandlers()));
Test it without the cloud:
// StartUp is your own class (derived from BenzeneStartUp) where host wiring like
// UseAwsLambda(...) above lives - boot it in memory, no cloud account or emulator:
var host = new AwsLambdaBenzeneTestHost(
BenzeneTestHost.Create<StartUp>()
.WithServices(s => s.AddScoped(_ => mockGreeter.Object))
.BuildAwsLambdaHost());
var response = await host.SendApiGatewayAsync(request);
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 @benzenejs/express @benzenejs/core-message-handlers @benzenejs/http @benzenejs/results
The @benzenejs/* packages aren't published to npm yet, so this command won't resolve today — it's the package set you'll install once they ship. Until then, build inside the repo's npm workspace; the getting-started guide in the docs shows how.
@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.
That's the two wiring lines above, drawn out: three different transports, one handler. Add a fourth transport and the handler still doesn't change.
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 — a named operation like order:create, not any one endpoint or queue — 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.
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.
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.
Spec UI
A Swagger-UI-style browser for a sample Benzene message spec — topics, payloads, and validation rules.
Built for production, not just prototypes
The quickstart above is the pitch; 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 →