zevOS

01Technology

Why a charging platform should be event-driven

The OCPP connection is real-time and fragile; billing is not. Putting an event log between them is the architectural decision that makes everything downstream tractable.

ENzevOS Engineering · Platform engineering
4 February 2026 · 2 min read

The tempting design is direct: a charger sends StopTransaction, the handler computes the price, charges the card, updates the session, notifies the driver and returns. It works for fifty chargers and becomes the reason for every outage after that.

What is wrong with it

  • The payment gateway is now on the critical path of a WebSocket handler. When it is slow, chargers time out.
  • A failure anywhere in the chain leaves the session half-processed, with no defined state and no way to resume.
  • Every new consumer of session data — analytics, settlements, a partner webhook — is another thing that can break the protocol handler.
  • The gateway cannot be scaled or deployed independently of the billing logic, because they are the same process.

The boundary that fixes it

The gateway does three things and no more: validate the message against the schema, normalise it into an internal vocabulary, and emit an event. It does not write to the business database, it does not price anything and it does not authorise anything beyond asking. Its latency budget is then bounded by the protocol, not by whatever the slowest downstream dependency happens to be today.

Everything else consumes those events: a billing consumer, a settlement consumer, an analytics consumer, a notification consumer. Each can fail, retry and be redeployed without the chargers noticing.

What it demands in return

Idempotency, everywhere. At-least-once delivery means a consumer will eventually see the same event twice, and a billing consumer that is not idempotent will eventually charge somebody twice. Every consumer needs a deterministic key and a check.

ConcernRequirement
Duplicate deliveryIdempotency key per event, checked before side effects
OrderingPer-charger ordering, not global — sessions on one unit must not interleave
Poison messagesA dead-letter path, or one bad event blocks a partition forever
Unacknowledged workConsumers reclaim pending messages on startup, or work is silently lost on a crash
Slow consumersA timeout around processing, so one stuck handler cannot deadlock the group

The last two rows are the ones that get discovered in production. A consumer that crashes with messages in flight and never reclaims them loses sessions quietly. A consumer that blocks forever on an exhausted connection pool stops processing without ever reporting an error.

It is not a message queue question

Redis streams, Kafka, NATS or RabbitMQ will all carry this. The choice affects operational effort and retention, not correctness. The decision that matters is the boundary — that the protocol handler emits and nothing else — and it is worth making on day one, because retrofitting it means rewriting every handler you have.

architectureOCPPscale

Get the next one by email

One email a month. Operations notes, unit economics and policy changes that affect your tariff.