Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk11 min

Building an Event-Driven Restaurant System with Node.js and Kafka

A practical architecture guide to modelling a restaurant order lifecycle as Kafka events in Node.js: keys, consumer groups, duplicates, payments and the real limits of exactly-once.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A restaurant order is a good teaching case for event-driven design. It passes through several stages: accepted, paid for, cooked, ready, and announced to the customer. Different teams and systems care about each stage. The pattern that works with Node.js and Kafka is this: the order service records what happened as events on a Kafka topic, keyed by order ID. Each interested system (payment, kitchen, notifications, analytics) reads that stream in its own consumer group, at its own pace. Because Kafka delivers at least once by default, every consumer must tolerate seeing the same event twice.

The workflow below is an illustrative design, not a description of any real restaurant’s deployment. The Kafka behaviour comes from the Apache Kafka documentation. The code uses KafkaJS, a Node.js client. Check any snippet against the client and broker versions you actually run.

As an Amazon Associate I earn from qualifying purchases.

The order lifecycle as events

Start with facts that have already happened, named in the past tense. An event says “this occurred”. It does not say “please do this”. That keeps producers ignorant of who is listening.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Event Published by Typical consumers
OrderPlaced Order service, after validating the request and assigning an order ID Payment, analytics
PaymentAuthorized / PaymentDeclined Payment component, after the provider responds Order service (to update status), kitchen, analytics
PreparationStarted Kitchen component, when staff or a kitchen display accepts the ticket Order service, customer-facing status, analytics
OrderReady Kitchen component Notifications, pickup or delivery dispatch, analytics
CustomerNotified (optional) Notification component Analytics, support tooling

Real menus add more branches: cancellations, refunds, item substitutions, delivery handoffs. Model the failure paths (PaymentDeclined, OrderCancelled) from the start. Retrofitting them into a happy-path-only stream is painful.

A minimal event envelope

Kafka records have a key, a value, a timestamp and optional headers. Put routing and tracing data in the envelope and the business payload inside it:

{
  "eventId": "6f1c0c1e-6a52-4a0c-9d7e-2d1f3b1d9a10",
  "type": "OrderPlaced",
  "schemaVersion": 1,
  "orderId": "ord_20481",
  "occurredAt": "2026-10-06T11:42:07Z",
  "data": {
    "items": [{ "sku": "burrito-chicken", "qty": 2 }],
    "totalCents": 2350,
    "currency": "USD"
  }
}

The eventId is what consumers use to detect duplicates. The schemaVersion gives you a way to evolve payloads later. Keep personal data (name, phone number) out of events that many consumers read, unless you have decided those consumers need it.

What each part is responsible for

Order API / order service

This is the only synchronous, customer-facing piece. It validates the request, assigns the order ID, stores the order, publishes OrderPlaced, and responds. It should not call the payment provider, the kitchen and the SMS gateway in sequence before answering. Doing so recreates the tight coupling the event stream is meant to remove. It also keeps a status view of each order, updated by consuming later events, so the app can show “preparing” or “ready”.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Topics

Kafka stores records in topics. Topics are always multi-producer and multi-subscriber, in the words of the Apache Kafka documentation, and are partitioned and retained according to topic settings. For this workflow, a single order-events topic holding every lifecycle event is a sound default. The reason is ordering, covered next. You might add separate topics for things that are not order lifecycle facts, such as menu-updates or inventory-events.

Producers

Any service can publish. The order service publishes OrderPlaced. The payment component publishes the payment outcome. The kitchen component publishes preparation events. Each producer owns the events it is authoritative for, so the payment component, for instance, is the only source of PaymentAuthorized.

Consumer groups

A consumer group is a logical subscription. Kafka delivers each record to one consumer instance within a group, and to every group that subscribes. Give each concern its own group (payments, kitchen, notifications, analytics) and each gets the full stream. Scale a group by adding instances, up to the number of partitions. A slow analytics job then cannot hold back kitchen tickets, because lag is tracked per group.

Because the log is retained, a consumer group added months later can start from the oldest retained record and build its own view. How far back that goes depends on the topic’s retention settings, so set them deliberately if replay matters to you.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Key by order ID to keep one order’s events in sequence

Kafka guarantees ordering within a partition, not across a topic. Records with the same key are routed to the same partition. Using the order ID as the key therefore means any consumer sees OrderPlaced before PaymentAuthorized before OrderReady for that order. Events for different orders may interleave, and that is fine, because no restaurant process needs a global order across all customers.

const { Kafka } = require('kafkajs');

const kafka = new Kafka({ clientId: 'order-service', brokers: ['broker1:9092'] });
const producer = kafka.producer();

async function publish(event) {
  await producer.send({
    topic: 'order-events',
    messages: [{
      key: event.orderId,
      value: JSON.stringify(event),
      headers: { type: event.type }
    }]
  });
}

Two consequences to plan for. First, if events for one order go to different topics, Kafka gives no ordering between them, which is another reason to keep the lifecycle on one topic. Second, changing the partition count later changes where a key lands, so pick a partition count with headroom up front.

The dual-write problem at the order endpoint

The order service must both save the order and publish OrderPlaced. A database commit and a Kafka publish are separate operations. If the database commit succeeds and the process dies before the publish, the order exists but nobody downstream hears about it. If you publish first and the commit fails, downstream systems act on an order that does not exist.

A common remedy is the transactional outbox. In one database transaction, write the order row and a row in an outbox table describing the event. A separate relay process (a poller, or a change-data-capture tool) reads unsent outbox rows, publishes them to Kafka, and marks them sent. The database transaction guarantees the order and its event are saved together. The relay can crash between publishing and marking, so it may publish an event twice. That is acceptable as long as consumers are idempotent, which is the next topic. This section describes the pattern conceptually. The exact mechanics depend on your database and tooling.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Delivery guarantees: plan for duplicates

Kafka’s baseline is at-least-once delivery. A consumer that processes a record and crashes before committing its offset will receive that record again after restart or a rebalance. Producer retries can also write duplicates unless idempotence is enabled. Treat duplicates as normal.

Making consumers idempotent

  • Record processed event IDs. Insert the eventId into a table with a unique constraint in the same database transaction as the state change. If the insert conflicts, skip the event.
  • Use state-machine guards. Only move an order from PLACED to PAID. A repeated PaymentAuthorized for an order already PAID becomes a no-op. This also protects you from out-of-date events after a replay.
  • Make writes naturally idempotent. “Set status to READY” is safe to repeat. “Increment the daily sales counter” is not, unless it is tied to a deduplicated event ID.
  • Commit offsets after the work is done. That gives at-least-once behaviour. Committing before processing risks silently losing events on a crash.
const consumer = kafka.consumer({ groupId: 'kitchen' });
await consumer.connect();
await consumer.subscribe({ topic: 'order-events', fromBeginning: false });

await consumer.run({
  eachMessage: async ({ message }) => {
    const event = JSON.parse(message.value.toString());
    if (event.type !== 'PaymentAuthorized') return;

    await db.tx(async (tx) => {
      const first = await tx.insertProcessedEvent(event.eventId, 'kitchen'); // unique key
      if (!first) return;                  // duplicate: already handled
      await tx.createKitchenTicket(event.orderId, event.data);
    });
  }
});

The helper names (db.tx, insertProcessedEvent) are placeholders for your own data layer. The client calls follow the basic KafkaJS workflow of client, producer or consumer, connect, send or subscribe, and run.

Payments need extra care

The payment effect lives outside Kafka. When the payment component consumes OrderPlaced and calls a provider, a redelivered record could charge twice. Use the provider’s idempotency mechanism if it offers one, deriving the key from the order ID so retries map to the same operation. Also store the outcome before publishing the result event, and run a reconciliation job that compares your records with the provider’s. A timeout from the provider is ambiguous: the charge may or may not have happened, so query its state rather than assuming failure. Payment compliance and data-handling rules vary by country and by provider. This article does not cover them.

What “exactly once” does and does not cover

Kafka offers idempotent producers and transactions. Transactions let a consume-transform-produce step write output records and commit the consumed offsets atomically, so downstream Kafka consumers reading committed data see the result once. The KafkaJS transaction guide describes this and requires Kafka 0.11 or later. Its documented setup uses a transactional ID, idempotence enabled, and a limit of one in-flight request.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const producer = kafka.producer({
  transactionalId: 'order-enricher-1',
  idempotent: true,
  maxInFlightRequests: 1
});
await producer.connect();

// inside the processing step for one consumed record
const tx = await producer.transaction();
try {
  await tx.send({ topic: 'order-events', messages: [/* derived event, keyed by orderId */] });
  await tx.sendOffsets({
    consumerGroupId: 'order-enricher',
    topics: [{ topic: 'order-events',
               partitions: [{ partition, offset: (Number(message.offset) + 1).toString() }] }]
  });
  await tx.commit();
} catch (e) {
  await tx.abort();
  throw e;
}

Confirm the option names and the offset semantics against the KafkaJS release you use. The boundary matters more than the syntax. A Kafka transaction covers records and offsets inside Kafka. It does not atomically include your restaurant database write, the payment provider call, a ticket printer, or an SMS gateway. Each of those has its own consistency and idempotency boundary. So do not call the whole workflow exactly-once. Call it at-least-once delivery with idempotent handlers, plus Kafka transactions for the Kafka-to-Kafka steps where you need them.

Retries, poison records and replay

Retry policy

Separate transient failures (database timeout, provider unavailable) from permanent ones (malformed event, unknown menu item). Transient failures deserve bounded retries with backoff. Blocking a partition with endless retries delays every order sharing it. A common design publishes failed records to a retry topic with a delay, or pauses the partition briefly, then moves on.

Poison records

A record that can never be processed must not stall the consumer forever. After a set number of attempts, publish it to a dead-letter topic with the error and original coordinates (topic, partition, offset), alert someone, and continue. Moving past a record breaks strict per-order sequencing for that order, so have the state-machine guards described above reject or park later events that arrive without their predecessors.

Schema evolution

Producers and consumers deploy independently, so old and new versions coexist. Prefer additive changes: new optional fields, never repurposed ones. Bump schemaVersion for breaking changes and let consumers ignore versions they do not understand. A schema registry with Avro or Protobuf enforces compatibility rules mechanically. For a small team, JSON with a versioned envelope and contract tests is a reasonable start.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Replay

Replay is powerful for projections: rebuild an analytics table by starting a new consumer group from the beginning. It is dangerous for side effects. A notification consumer replayed from the start would text every past customer. Keep side-effecting consumers separate from projection consumers, and give side-effecting handlers a “have I already done this for this event ID” check that survives offset resets.

What to monitor

  • Consumer lag per group. Rising lag on kitchen means tickets are late.
  • Dead-letter topic volume, and the age of the oldest unhandled item.
  • Time from OrderPlaced to OrderReady, computed from event timestamps, as a business-level health check.
  • Orders stuck in an intermediate state (for example paid but no preparation event after N minutes). This catches lost events that lag metrics miss.
  • Rebalance frequency. Frequent rebalances usually point to slow handlers or unstable instances.
  • Trace propagation. Carry an order or correlation ID in headers and logs so one order can be followed across components.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

One application or several services?

Kafka supports event-driven architectures and microservices, but its documentation does not say a restaurant needs separate services, and nothing here implies a scale threshold. You can use Kafka’s decoupling inside a single deployable Node.js application, with separate consumer groups for each concern running as modules. You can later split those modules into services without changing the event contracts.

Concern Single app, async internally Separately deployed services
Operational complexity One pipeline, one set of logs and configs Several pipelines, versioning and service discovery to manage
Deployment independence One release unit; a notification change redeploys everything Teams ship kitchen or notification changes separately
Failure isolation A crash or memory leak takes down every module A failing notifier does not affect kitchen processing
Scaling Scale the whole app together Scale hot components separately, within each group’s partition limit
Testing Easier end-to-end tests in one process Needs contract tests for event schemas

A single restaurant or a small group usually gains more from clean module boundaries and event contracts than from many deployables. Separate services pay off when different teams own components, release cadences diverge, or a component’s load or failure profile differs sharply. Ask also whether you need Kafka at all. A job queue or database-backed queue may be enough if you do not need multiple independent subscribers, retained history or replay.

Self-managed or managed Kafka

Apache’s documentation covers both running Kafka yourself and consuming it as a managed service. Neither is universally right. The decision turns on factors you can assess for your own situation:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Operations capacity. Running brokers means handling upgrades, storage, monitoring, and failure recovery. A managed service shifts much of that to a provider.
  • Control and environment. Self-managing gives control over configuration and placement, such as on-premises or inside a restricted network.
  • Security and data requirements. Check encryption, authentication, network isolation and data-residency options before choosing.
  • Availability needs. A restaurant that cannot take orders during an outage needs a multi-broker setup and a tested failure plan either way.
  • Cost. Compare current provider pricing and service-level terms for your own traffic. Those terms change and are not covered here.

For local development, a single-broker setup is enough to exercise topics, keys, consumer groups and replay. It does not exercise broker failure.

Node.js client considerations

KafkaJS is an Apache Kafka client for Node.js with producer, consumer-group and transactional support. Before committing, check its current release, maintenance activity and compatibility with your broker version on the project’s repository. Other Node.js clients exist, and they differ in API and operational behaviour.

  • Handlers are asynchronous. Keep them non-blocking, and avoid CPU-heavy work in the same process that holds a consumer session, because slow handlers can trigger rebalances.
  • Reuse one producer per process rather than connecting per request.
  • Handle shutdown signals by disconnecting the consumer, so in-flight work finishes and offsets are committed before the process exits.
  • Pin and test the client version. Defaults and option names can differ between releases.

A sensible build order

  1. Define the event envelope and the five lifecycle events, plus declined and cancelled paths.
  2. Create order-events with a partition count that leaves headroom, and key everything by order ID.
  3. Build the order endpoint with an outbox (or consciously accept the dual-write risk for a prototype).
  4. Add consumers one at a time: payment with provider idempotency keys, then kitchen, then notifications. Make each idempotent before moving on.
  5. Add retry, dead-letter handling and lag alerts before you add features.
  6. Add an analytics consumer group reading from the beginning to prove replay works.
  7. Split into separate services only when a concrete deployment, ownership or isolation need appears.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.