Skip to content
All posts
4 min readAugust 1, 2026English
JH

Jubayer Hossain

Founding Product Engineer

Architecting a Marketplace: 1 Backend, 5 Surfaces

How I engineered a unified backend to serve a driver app, admin panel, supplier portal, customer website, and a WhatsApp bot without collapsing into spaghetti code.

architecturebackendexpressbullmq

The naive way to build a multi-sided marketplace is to spin up a separate API for every client surface. You build a Driver API, a Customer API, and an Admin API. Six months later, your business logic is scattered across three codebases, a bug fixed in one API still exists in the other two, and your database is a corrupted mess of race conditions.

When I architected a complex logistics and trucking marketplace—I knew we needed a single source of truth. The platform had to serve five entirely different actors:

  1. Driver App (Mobile)
  2. Supplier Portal (Web)
  3. Customer Website (Web)
  4. Admin Dashboard (Web)
  5. WhatsApp Bot (Conversational AI)

Here is how I structured the system to handle this complexity without losing engineering velocity.

1. The Unified Core

The architecture revolves around a single Node.js/Express backend backed by MongoDB. Instead of splitting by client, the API is split by domain.

If a load needs to be updated, the Driver App and the Admin Panel hit the exact same underlying LoadService. The controllers might map the request differently based on role-based access control (RBAC), but the core mutation logic lives in exactly one place.

┌─────────────────┐   ┌───────────────────┐   ┌─────────────────┐
│  Driver App    │   │  Supplier Portal   │   │  Admin Panel   │
└────────┬────────┘   └─────────┬─────────┘   └────────┬────────┘
         │                    │                    │
         └───────────────────┴───────────────────┘
                             │
                    ┌────────┴────────┐
                    │ Express API     │
                    │ Gateway         │
                    └────┬──────┬────┘
                         │      │
                ┌────────┴┐  ┌──┴────────┐
                │ MongoDB │  │ Redis /   │
                │         │  │ BullMQ    │
                └─────────┘  └───────────┘

Two additional surfaces — the WhatsApp Bot (via webhooks) and the Customer Website — also feed into the same API gateway.

2. Decoupling High-Latency Side Effects

Logistics platforms generate a massive amount of asynchronous work. When a load is delivered, the system needs to:

  • Generate a PDF invoice (via Puppeteer/PDFme)
  • Calculate density updates
  • Update the recommendation engine for future bids
  • Trigger WhatsApp notifications to the supplier

If you run these side effects synchronously in the HTTP request lifecycle, your API will eventually choke and timeout.

I decoupled every single heavy operation using BullMQ and Redis.

// A simplified look at how jobs are enqueued in the logistics platform
import { Queue } from 'bullmq';
import { redisConnection } from '../config/redis';

// 1. Define the Queues
export const pdfGenerationQueue = new Queue('pdf-generation', { connection: redisConnection });
export const waNotificationQueue = new Queue('wa-notification', { connection: redisConnection });

// 2. Enqueue the work immediately, return 200 OK to the client
export const handleLoadDelivery = async (req, res) => {
  const { loadId, driverId } = req.body;
  
  // Update core state synchronously
  const load = await LoadService.markDelivered(loadId, driverId);
  
  // Offload heavy side-effects
  await pdfGenerationQueue.add('generate-invoice', { loadId: load.id });
  await waNotificationQueue.add('notify-supplier', { supplierId: load.supplierId, loadId: load.id });
  
  return res.status(200).json({ status: 'success' });
};

By pushing these to isolated worker processes (pdf-generator.worker.js, wa-notification.worker.js, etc.), the main API remains incredibly fast and responsive, even under heavy load. If Puppeteer crashes while generating a PDF, the worker simply retries the job. The driver who submitted the delivery never sees a 500 error.

3. Webhooks and The WhatsApp Bot

Integrating a WhatsApp Bot acting as a client surface flips the traditional request/response model. Instead of the client polling us, Meta's servers send webhooks to our API.

Webhooks are notorious for arriving late, out of order, or exactly twice. If a supplier confirms a bid via WhatsApp, and the webhook fires twice, a naive system will double-process the bid.

To solve this, the webhook receiver is strictly idempotent. It verifies the signature, checks a unique constraint on the provider_event_id in the database, and if it’s a duplicate, it silently drops it. If it's valid, it parses the intent and routes the payload to the exact same internal BiddingService that the Supplier Web Portal uses.

The Takeaway

Building for five different clients doesn't mean writing the same logic five times. By centralizing the domain logic in a single Express backend, strictly enforcing RBAC at the controller level, and ruthlessly offloading side-effects to BullMQ, you can scale a massive multi-sided platform with a shockingly lean engineering team.

Have a product to build?

I take AI-native products 0→1 — from first commit to first paying user. Let's talk about your next build.

Start a project