Event-Driven Architecture Consulting & Development

Event-driven architecture lets a product react to facts instead of calling the next service and hoping the whole chain stays up. Checkout should not wait for inventory, email, invoicing and analytics to each answer in turn. Those are consequences of “the order was placed”, and each of them should be free to succeed, retry or fall behind without taking the customer’s request down with it.

At CodeWiz we design the events, choose the broker and build the consumers. That covers event sourcing where the history is the source of truth, message brokers for work that must be delivered, event streaming when several teams need to replay the same facts, and real-time processing for the parts of the product a person is actually watching. We work in Laravel, Node.js and existing systems that already have a database and a pile of cron jobs polling it.

This service is for online stores, SaaS products with integrations, and any workflow where one user action has to fan out to several outcomes. If you have a single background job and one worker, a simple queue is enough, and we will say so.

Event Sourcing: State as a History of Facts

Event sourcing stores what happened, in order, and treats the current row as a view you can rebuild. OrderPlaced, PaymentCaptured and ShipmentDispatched are the record. A fold over that stream produces the order you show today. The alternative – update a row, then hope a side table kept the story – loses the steps the moment someone “fixes” the row by hand.

When the log earns its place

  • The business asks “what did this look like on Tuesday?” or an auditor does.
  • Several consumers need different shapes of the same facts: a search index, a ledger, a notification, a warehouse screen.
  • Corrections are new facts (RefundIssued), not silent edits of the past.
  • You can name the events in the language of the business. If the only honest name is RowUpdated, you do not have events yet.

We keep streams per aggregate, append with an expected version so two writers cannot overwrite each other, and snapshot only after a stream is long enough that replaying it on every load is wasteful. Event sourcing is not the default storage for every table. A settings screen does not need a log. Losing a customer action in a checkout flow does, which is the same bar we describe in making sure every order and customer action can be reconstructed.

Event: Bundle of orange, blue and grey network cables with exposed connectors

Message Brokers for an Event-Driven Architecture

A message broker moves a fact from the service that recorded it to the services that care. The wrong choice is rarely about peak benchmarks. It is about whether you need a queue of tasks, a durable log many teams can read at their own pace, or a small low-latency bus. We pick one primary broker per system and add a second only for a reason we can explain.

BrokerModelStrengthWatch out for
RabbitMQExchanges, queues, competing consumersTask distribution, routing, retries, dead-letter queuesMessages are consumed, not kept as a long-term log you can replay freely
Apache KafkaDurable log, partitions, consumer groupsHigh throughput, retention, many independent readers, replayPartitions, retention and consumer lag are now part of the design
NATS and JetStreamSubjects, lightweight pub/sub, optional durable streamsLow latency and a small operational footprintLess surrounding tooling when the processing itself becomes heavy
Redis StreamsA log inside Redis, consumer groupsYou already run Redis and the volume is modestNot a stand-in for Kafka once retention and fan-out grow

How we run them

  • A dead-letter destination for messages that fail for a reason a retry will not fix, plus an alert on its depth.
  • Bounded retries with backoff. An infinite retry loop is how one poison message stalls a partition.
  • Consumers that are idempotent, because delivery is at least once in every broker worth using.
  • Metrics for lag, publish failures and consumer crashes, on the same footing as the rest of the application.

The references we configure against are the RabbitMQ documentation, the Apache Kafka documentation and the NATS documentation. Where the brokers themselves run – one VPS, or a layout across providers – follows the same judgement as the rest of the platform, which we have written about in deployment across DigitalOcean, AWS, Hetzner and bare metal.

Event Streaming That Survives a Restart

Event streaming is the part of an event-driven architecture you have once the broker keeps an ordered, retained log and consumers remember their position. A new service can start at the beginning of the month and build its own read model. A bug in a consumer can be fixed and the same facts read again. That is a different promise from a queue, where a message is gone once someone acknowledges it.

Ordering, keys and replay

  • Order is guaranteed per key, not globally. We key by aggregate ID (the order, the account), so one entity’s events stay in sequence on one partition.
  • Consumer groups share the work. Two instances of the billing consumer split partitions; a separate group for search reads every partition itself.
  • Replay is a planned operation: a new consumer group or a reset offset, against a projection that can be emptied and rebuilt.
  • Retention is a product decision. Seven days is enough for retry and a short rebuild; a ledger that must be replayable for years belongs in the event store, with the broker as the delivery path.
Clear mountain stream rushing over smooth rocks in a green forest

Real-Time Processing That Does Not Block the User

Real-time processing means a person, or another system, sees the consequence quickly enough for the job at hand. For a chat message that is a fraction of a second. For an order status it might be a second or two. For a nightly settlement it is not real-time at all, and forcing it to be only adds cost. We put the latency budget in writing before choosing the tool.

The last hop is not the broker

  • The request that places the order commits the fact and returns. It does not call every downstream system.
  • Consumers update the read models and the notification outbox. A WebSocket or server-sent events connection, where the product needs a live screen, is the last step and can reconnect.
  • A live dashboard reads a projection, not the write transaction. The chart can be a second behind; the charge cannot be applied twice.
  • Backpressure is visible. If the warehouse consumer falls behind, the site still takes orders, and someone is paged about the lag.
Monitor displaying live market charts and constantly updating numbers

Designing Events: Names, Versions and Idempotency

An event is a fact in the past tense, with a stable name and a payload of what a consumer needs in order to act. We version the payload from the first release. Adding a field is safe for consumers that ignore unknowns; renaming or changing a meaning is a new version, and both versions are produced until every consumer has moved.

  • A unique event ID and the command or request ID that caused it, so a replay does not look like a new fact.
  • The aggregate ID and its version, so consumers can drop anything that arrives out of the sequence they have already applied.
  • No secrets and no large blobs. A payment event carries the provider reference and the amount, not the card and not a PDF.
  • Consumers store the event IDs they have handled. At-least-once delivery then becomes a no-op instead of a double email or a double charge.

Payment is the sharp case. “Captured” has to be processed once even when the broker delivers it twice, which is why idempotency sits next to the gateway integration we describe in payment gateway integration for online stores.

The Transactional Outbox

The classic failure is a dual write: the database commits, and the publish to the broker does not, or the other way around. The transactional outbox puts the event in the same database transaction as the state change. A separate relay reads unpublished rows and publishes them. If the process dies, it publishes again. Consumers are already idempotent, so the duplicate is harmless.

CREATE TABLE outbox ( id uuid PRIMARY KEY, aggregate_id uuid NOT NULL, type text NOT NULL, payload jsonb NOT NULL, occurred_at timestamptz NOT NULL, published_at timestamptz );

The relay claims a batch, publishes, then sets published_at. A row that stays unpublished past a threshold is an alert, not a surprise discovered by a customer. We use the same table shape in PostgreSQL for Laravel and for Node.js services; the relay is a small worker, not a second framework.

When an Event-Driven Approach Is the Wrong Tool

Events add operational surface: lag, poison messages, versioning, replay. They pay for that when there is real fan-out, a real need to replay, or a real boundary between teams. They do not pay for a single email sent after signup. In that case a job on a Redis or database queue, in the same application, is the whole design. We would rather run that than a cluster with one consumer.

Common Event-Driven Architecture Mistakes We Fix

  • Publishing after the commit, in application code, with no outbox and no recovery when the process crashes in between.
  • Events that carry the entire row under a name like Updated, so nobody can version them or tell what happened.
  • Consumers that are not idempotent, paired with a broker that will happily deliver twice.
  • Assuming a global order across all partitions, then debugging it in production.
  • Retries without a dead-letter path, so one bad payload blocks everything behind it.
  • A “real-time” requirement that was never given a number, implemented as synchronous calls to every downstream service.

How We Work

  1. Discovery: we list the facts the business cares about, who must react, and how late each reaction is allowed to be.
  2. Design: event names and payloads, the broker, the key used for ordering, and where an outbox is required.
  3. Build: publishers, the relay, consumers, dead-letter handling and the idempotency store.
  4. Prove it: a killed process mid-publish, a duplicate delivery, a replay into an empty projection, and a consumer that falls behind on purpose.
  5. Launch: one flow first, beside the old path, until the new consumers match it. Then the old chain comes out.
  6. Support: lag and dead-letter alerts, a written way to add a consumer, and a versioning rule for the next payload change.

Why CodeWiz

  • We build both sides: the Laravel or Node.js code that records the fact and the consumers that react, so the contract is not theoretical.
  • The smallest broker that fits: Redis or RabbitMQ when that is honest, Kafka or NATS when replay or latency actually requires it.
  • Open-source first: Kafka, RabbitMQ, NATS and PostgreSQL, on infrastructure you can leave with. No per-message tax as the entry fee.
  • Full-stack: APIs, databases, queues and the live screen are one design, because the same team ships them.
  • Fair pricing: premium development without premium prices, from our team in Banja Luka, working remotely with clients locally and internationally. More about CodeWiz.

Frequently Asked Questions

How much does an event-driven architecture cost?

A single outbox, one broker and two consumers on an existing application is a focused project. Event sourcing several aggregates, with replayable projections and a retention story, is a larger one. Most of the extra time is proof: failure tests, a timed rebuild and the migration off the old synchronous chain. We estimate after we know how many facts and consumers are in scope.

Should we use Kafka or RabbitMQ?

Use RabbitMQ when you are distributing tasks, need routing and dead-letter queues, and do not need to replay a long history. Use Kafka when several independent consumers must read the same facts, or you need to rebuild a projection from retained data. Plenty of products run well on RabbitMQ or Redis Streams for years.

Do we have to adopt event sourcing?

No. Publishing an event when a normal database transaction commits, via an outbox, is enough for most fan-out. Event sourcing is for the cases where the history itself is the source of truth. We do not replace a working relational model just to have streams.

How do you keep messages from getting lost?

The event is written in the same transaction as the state change, a relay publishes until the broker acknowledges, and consumers record what they have already applied. A crash at any point retries. The remaining risk is a consumer that applies a side effect and then crashes before recording it, which is why the side effect itself has to be idempotent too.

Can you introduce this without rewriting the application?

Yes. We usually wrap one important flow: the state change and the outbox land in the current codebase, and one new consumer takes over a single downstream job. The rest of the synchronous calls stay until that path has earned the same treatment.

Ready to React to Facts Instead of Chains of Calls?

Tell us which user action fans out today, and which step is the one you are afraid to retry. We will propose a broker and an event design that match that problem. Contact us to talk it through, or see our event-driven architecture services.