MicroTix

Event-Driven Ticket Marketplace on a Production-Grade Microservices Architecture

Video Demo

Gallery

MicroTix interface gallery

The Next.js client that exercises the system — sign-in, ticket creation, the reservation window, and expiration in action.

01 / 09

MicroTix Landing page

Landing page

Project Overview

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.

System Architecture

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.

Loading diagram…

Services

  • TypeScript
  • Node.js
  • Express.js
  • MongoDB
  • Mongoose
  • JWT
  • Stripe

Messaging

  • NATS Streaming
  • Redis
  • Bull (job queue)
  • @akmicrotix/common

Infrastructure

  • Docker
  • Kubernetes
  • NGINX Ingress
  • Skaffold
  • GitHub Actions
  • DigitalOcean

The Services

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.

auth

Owns identity — sign up / sign in and mints stateless JWTs validated locally by every other service.

tickets

Owns tickets — create, edit, and list; locks a ticket on reservation and emits TicketCreated / TicketUpdated.

orders

Owns orders — reserves a ticket for 15 minutes, rejects double-booking, and drives the order state machine.

expiration

A pure worker with no API — schedules a delayed Bull job and emits ExpirationComplete when a reservation times out.

payments

Owns payments — creates a Stripe charge against replicated order data and emits PaymentCreated.

client

A Next.js app that exists only to exercise the system end-to-end through the ingress.

Event Flow Through the Bus

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.

Loading diagram…

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.

Concurrency & Consistency

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.

Loading diagram…

Order Lifecycle & Expiration

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.

Loading diagram…

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.

User Flow

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.

Loading diagram…

Testing Strategy

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.

In-memory Mongo per test suite — no shared or persistent state between runs
Mocked NATS client to assert publish calls and payloads
Supertest against each service's Express app for route-level coverage
A dedicated concurrency test proves out-of-order events heal via redelivery

Key Features

Six independently deployable services, each with its own private database
Zero synchronous service-to-service calls — coordination happens only through events
Custom @akmicrotix/common npm package as the single, versioned event contract
NATS Streaming event bus with durable subscriptions and manual acks (at-least-once)
Optimistic concurrency control with per-record versions to survive races
Out-of-order events self-heal through redelivery instead of corrupting state
15-minute reservation window enforced by a Redis + Bull delayed job
Full Kubernetes deployment with NGINX ingress and Skaffold hot-reload for dev

Challenges & Solutions

Challenge: Keeping data consistent without a shared database

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.

Challenge: Events arriving late, twice, or out of order

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.

Challenge: Two users reserving the same ticket at once

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.

Challenge: Sharing types and event definitions across services

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.

Challenge: Cancelling orders that were never paid

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.

Project Stats

Services
6
Private Datastores
5
Event Types
6

Technologies Used

TypeScriptNode.jsExpress.jsMongoDBMongooseJWTStripeNATS StreamingRedisBull (job queue)@akmicrotix/commonDockerKubernetesNGINX IngressSkaffoldGitHub ActionsDigitalOcean

Architecture Highlights

Database-per-service
Event-driven (NATS Streaming)
Optimistic concurrency control
Shared npm event contract
Kubernetes-native