Airbnb - Distributed Booking

A team-built Airbnb-style booking platform that grew from a monolith into event-driven microservices: a React/Redux frontend over four Node services and a Python AI agent, talking through Kafka with MongoDB, and deployable on Kubernetes.

ROLE
Developer
PERIOD
2025
DOMAIN
Distributed Systems
STATUS
Published

OVERVIEW

A team-built Airbnb-style booking platform, internally called Voyage, that evolved from a monolith into an event-driven microservices system. A React/Redux/Vite frontend sits over four Node/Express services (traveler, owner, property, booking), each with its own MongoDB, communicating through Kafka topics with consumer groups and retry rather than direct calls. A separate Python FastAPI service runs a local Ollama model through LangChain to generate JSON travel itineraries, kept isolated from the booking path. The whole system ships with Docker Compose for local runs and a full set of Kubernetes manifests, plus Playwright, Postman, and JMeter for end-to-end, API, and load testing.

ARRIVED AS

An Airbnb-style app has clearly separable concerns, travelers, owners, properties, bookings, that fight each other inside a single codebase as it grows. The project's goal was to take a working monolith and re-architect it into independent services that communicate asynchronously, so a slow or failing service degrades the system instead of taking it down, and to make the whole thing deployable on Kubernetes.

This was a team distributed-systems project built in two iterations: a monolithic first version (Lab 1) and then a re-architecture into event-driven microservices (Lab 2). The product is an Airbnb-style booking site, travelers search and book properties, owners list and manage them, with an added AI travel-planning concierge. Internally the team called it Voyage. My focus was on the distributed back end: the services, their Kafka communication, and the deployment setup.

WHAT I BUILT

  1. 01A React + Redux + Vite frontend (Tailwind) with feature slices for auth, properties, bookings, favorites, and the owner dashboard, talking to the backend over a small API layer.
  2. 02Four Node/Express microservices, traveler, owner, property, and booking, each with its own responsibility and MongoDB data, deployable independently.
  3. 03Kafka as the event bus: the booking service consumes booking-request and booking-update topics with consumer groups and retry-on-failure, so booking status stays consistent across services without synchronous coupling.
  4. 04A separate Python FastAPI agent service that uses LangChain with a local Ollama model to generate day-by-day travel itineraries as structured JSON.
  5. 05Packaged for both local and cluster runs: Docker Compose for development and a full set of Kubernetes manifests, with Playwright end-to-end tests, a Postman collection, and JMeter load testing.

WHAT CHANGED

  • Re-architected from a Lab 1 monolith into a Lab 2 event-driven microservices system, the evolution itself is the point: the same product, decomposed and made resilient.
  • The booking flow tolerates a missing Kafka broker (it logs and runs degraded) and retries failed messages, so async consistency does not become a single point of failure.
  • An AI concierge built as its own service rather than bolted onto the frontend, keeping the LLM dependency isolated behind a clean API.

Data flow

click a stage

React/Redux pages call the service APIs through a small API layer; Redux slices hold auth, properties, bookings, favorites, and dashboard state.

COMPONENT

Traveler / Owner / Property / Booking services

Four Node/Express microservices, each owning its domain and MongoDB data, deployable independently on their own ports.

Decisions, with the cost of each.

A decision without its trade-off is marketing. Each row says what was chosen, why, and what it gave up.

Communicate through Kafka instead of direct service-to-service calls

Synchronous calls between services couple their availability: if one is down, the caller fails. Routing booking events through Kafka topics with consumer groups means a service can be slow or briefly offline and catch up from the log, which is the whole reason to go microservices in the first place.

Direct REST calls between services (tight coupling, cascading failures); a shared database (re-couples the services through their data).

Isolate the AI concierge as its own service

The LLM dependency (LangChain + a local Ollama model) is heavy and unrelated to booking correctness. Putting it behind its own FastAPI service keeps that dependency out of the core path, so the booking system does not inherit the AI service's footprint or failure modes.

Call the LLM from the frontend or a core service (drags the model dependency into the booking path and the client bundle).

Degrade gracefully when Kafka is absent

A broker is not always available in local development. The booking service logs a warning and runs without Kafka rather than refusing to start, so developers can work on the rest of the system without standing up the full event bus.

Hard-require Kafka at boot (blocks all local work on the service unless a broker is running).

The part that mattered.

The numbers behind the work, and the code that produced them.

deployables
6 services
4 Node + 1 Python agent + React frontend
event bus
Kafka
booking topics, consumer groups, retry
full manifests
Kubernetes
+ Docker Compose, Playwright, JMeter
local LLM agent
Ollama
LangChain JSON itineraries
The booking service consumes Kafka topics, with retryjavascript
async function setupKafkaConsumer() {
  const consumer = await kafka.createConsumer('booking-status-sync-group');
  if (!consumer) {
    logger.warn('Kafka consumer not created - running without Kafka');
    return;   // degrade gracefully in local dev
  }

  await kafka.subscribe('booking-updates', async (updateData) => {
    // apply the status update to this service's MongoDB
  });

  await kafka.subscribe('booking-requests', async (bookingData) => {
    // create the booking; on error, rethrow to trigger Kafka retry
  });

  await kafka.startConsumer();
}

Cross-service booking consistency runs over two Kafka topics on a consumer group. Handlers rethrow on failure so Kafka retries the message, and if no broker is present the service logs and runs degraded instead of failing to start, the same degrade-gracefully approach used elsewhere in the project.

The AI concierge: LangChain + a local Ollama modelpython
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOllama(
    base_url=config.OLLAMA_API,
    model=config.OLLAMA_MODEL,
    temperature=0.2,   # lower temp -> more deterministic JSON
)

itinerary_prompt = ChatPromptTemplate.from_template("""
You are an expert travel agent. Create a day-by-day itinerary for {location}
for {duration} days given preferences: {preferences}.
Return strictly a JSON array, one object per day, no markdown.
""")

The travel concierge runs a local Ollama model through LangChain, with a low temperature and a strict JSON-only prompt so the itinerary parses reliably. Running the model locally keeps the feature free of an external API key, and isolating it in its own service keeps that choice from leaking into the rest of the system.

✓ LEARNED

  1. Moving from a monolith to microservices is mostly a communication problem: the services are the easy part, deciding what goes over Kafka versus a direct call is where the design actually lives.

  2. Retry-on-failure plus a graceful no-broker path makes the event bus a resilience feature rather than a new single point of failure.

  3. Keeping the LLM in its own service paid off: the AI concierge could change models or break entirely without touching the booking flow.