Event-Driven Ticket Marketplace on a Production-Grade Microservices Architecture
Gallery
The Next.js client that exercises the system — sign-in, ticket creation, the reservation window, and expiration in action.
01 / 09

Landing page
MicroTix is a ticket-reselling marketplace where users list tickets for sale, reserve tickets from other users, and pay for them via Stripe. On the surface it behaves like any small e-commerce app — but underneath it is deliberately built as a distributed system to confront the real problems of microservices rather than fake them. This is a case study in architecture, not frontend polish; the Next.js client exists only to exercise the system end-to-end.
Instead of one server and one database, MicroTix is split into six independently deployable services, each with its own private database. No service reads another's data or calls it directly — they coordinate purely by publishing and subscribing to events on a NATS Streaming event bus, sharing a single source of truth for those events through a custom npm package I authored and published, @akmicrotix/common. The hard parts — how services agree without talking directly, and how they stay correct when messages arrive late, twice, or out of order — are solved with an event bus, a versioned contract, and optimistic concurrency control.
Every request enters through an NGINX ingress that routes by path to the right service. Services never call each other synchronously — the only line of communication between them is the NATS Streaming event bus, and each is backed by its own private datastore that nothing else can touch.
Each service has a single responsibility and a private database. Together they form the whole marketplace, but each could be deployed, scaled, or replaced on its own.
authOwns identity — sign up / sign in and mints stateless JWTs validated locally by every other service.
ticketsOwns tickets — create, edit, and list; locks a ticket on reservation and emits TicketCreated / TicketUpdated.
ordersOwns orders — reserves a ticket for 15 minutes, rejects double-booking, and drives the order state machine.
expirationA pure worker with no API — schedules a delayed Bull job and emits ExpirationComplete when a reservation times out.
paymentsOwns payments — creates a Stripe charge against replicated order data and emits PaymentCreated.
clientA Next.js app that exists only to exercise the system end-to-end through the ingress.
A single reservation ripples across four services purely through events — no service waits synchronously on another. NATS Streaming gives durable subscriptions and manual acks, so an event is redelivered until it has been successfully processed.
Reserving a ticket locks it, starts an expiration timer, and opens a payment window. The happy path completes with a Stripe charge; the timeout path cancels the order and releases the ticket.
At-least-once delivery means events can arrive late, twice, or out of order. The fix is optimistic concurrency control: every record carries a version, and every update event carries the version it expects. A consumer applies an event only if its version is the very next one after the local copy — an out-of-order event finds no matching document, so the listener refuses to ack it and NATS redelivers it later. The system self-heals instead of silently going wrong.
An order is a small state machine. When it's created, the expiration service schedules a delayed Bull job in Redis. If the buyer pays before the timer fires the order completes; if not, expiration triggers a cancellation and the ticket returns to the market.
The 15-minute window isn't a cron sweep or a polling loop — it's a single delayed job per order, held in Redis, that emits exactly one ExpirationComplete event when it fires.
What all that distributed machinery looks like from a user's seat: sign in, list or reserve a ticket, and either pay within the window or watch the reservation expire.
Each service is tested in complete isolation. An in-memory MongoDB spins up fresh for every run, and the NATS client is mocked so tests assert that the right events were published without needing a live bus — keeping tests fast, deterministic, and independent of infrastructure.
Solution: Each service owns a private database and never reads another's tables. Services publish domain events on every state change; consumers keep a lean local replica of only the fields they need, so consistency becomes eventual and event-driven.
Solution: Adopted optimistic concurrency control: every record carries a version and every update event carries the version it expects. A consumer only applies an event exactly one ahead of its local copy — otherwise it refuses to ack, and NATS redelivers it later once the predecessor lands.
Solution: Reservation queries for the ticket in an unreserved state as part of the write, and the mongoose-update-if-current plugin bumps the version atomically — so a concurrent second reservation matches nothing and is rejected.
Solution: Built @akmicrotix/common and published it to npm. It exports the Subjects enum, typed Publisher / Listener base classes, shared errors, and middlewares, so a breaking change to an event is a versioned bump the type system enforces at build time.
Solution: The expiration service enqueues a delayed Bull job in Redis keyed to each order's expiresAt. When it fires it emits ExpirationComplete, and orders cancels the order only if it is still awaiting payment, releasing the ticket back to the market.