Core Concepts

Status: DRAFT v0.1

This document defines Benzene's abstract model. Nothing here mentions a wire format (see wire-contracts.md) or a specific transport (see transport-bindings.md).

1. The model in one paragraph

A Benzene application is a set of message handlers, each identified by a topic, invoked through a middleware pipeline that operates on a transport-specific context. Transport adapters (ports, in hexagonal-architecture terms) convert a native transport event (an HTTP request, a gRPC call, a queue message, a function invocation) into a context, run the pipeline exactly once, and convert the handler's result back into a native response. Application code — handlers and middleware — is transport-neutral and vendor-neutral; only the adapter at the edge knows what the transport is.

That paragraph describes the steer, not the minimum: message handlers are Benzene's recommended shape, but a pipeline with no handlers at all, or one invoked in process behind no transport, is still a conforming Benzene application. Which capabilities each part unlocks — and how the ones that need handlers degrade without them — is defined in design-principles.md.

2. Topic

A topic identifies a message type and routes it to a handler.

Topic ids SHOULD be lower-case with : or _ separators (e.g. order:create, say_hello); this is a convention, not a requirement.

A topic's version (this section) selects between handler implementations registered for the same topic id. This is a distinct concept from a request/response payload's schema version (transparently upcast/downcast without a second handler) — see versioning.md (draft) for both, and how they relate.

3. Message handler

A handler is a function from a request to a result:

handle : TRequest -> Result<TResponse>

4. Middleware pipeline

A pipeline is an ordered list of middleware components executed around handler dispatch:

middleware : (Context, next: () -> Promise<void>) -> Promise<void>

Required semantics:

5. Result

Every handler invocation produces a result:

Field Type Meaning
status string One of the status vocabulary values (see wire-contracts.md §3), or an application-defined extension
isSuccessful boolean Derived from status class
payload TResponse? Present on success (and optionally on failure)
errors string[] Zero or more human-readable error messages; populated on failure

6. Context and request mapping

Each transport defines a context type carrying, at minimum:

Request mapping converts the context's native payload into the handler's declared TRequest:

  1. If the native payload already is TRequest (same type / directly assignable), pass it through untouched — zero-copy.
  2. If both sides are asynchronous streams, wrap lazily, converting per item by rules 1/3.
  3. Otherwise convert via the binding's serialization rule (JSON by default; protobuf-JSON for gRPC — see the bindings).

The same rules apply in reverse for the response. This is what lets a handler choose, per side, between the transport's native type (zero-copy) and its own plain type (converted).

7. Application lifecycle

An application is defined once, platform-neutrally, in three phases run in order, exactly once, at startup:

  1. getConfiguration() — produce the configuration object. Runs before any registration; no service resolution available.
  2. configureServices(container, configuration) — register handlers, middleware dependencies, and adapters with the DI container (section 8).
  3. configure(appBuilder, configuration) — build the pipeline(s) against a platform-neutral application builder. Transport-specific entry points are attached via use<Transport>(...) extensions that pattern-match the concrete platform and no-op otherwise, so one application definition can target several platforms and each deployment activates only the entry points its host supports.

The no-op rule is load-bearing for vendor neutrality: useAwsLambda(...) on an Azure host, or useGrpc(...) on a worker host, MUST silently do nothing rather than fail.

8. Dependency registration and resolution

Benzene defines a minimal container abstraction rather than mandating a DI framework:

9. Handler discovery

The concept is explicit registration: an application hands the framework a list of (topic, version, handler, request type, response type) records.

Attribute/annotation scanning ([Message("topic")] in .NET) is an idiom — per-language sugar that produces those records. Implementations MUST support explicit registration as a first-class path; scanning is optional. Route-level attributes for specific transports (e.g. [GrpcMethod("/pkg.Service/Method")]) are likewise sugar over an explicit (route → topic) registration.

10. Cross-cutting middleware guaranteed to be portable

These features are defined purely in terms of sections 2–8 and the header conventions in wire-contracts.md §2, so they MUST behave identically on every transport and every vendor: