System Design

How would you design a resource allocation app?

I built this system for a real problem: an ISP needed to connect new 10Gbps internet customers to the nearest available infrastructure chamber. The sales ...

24 Mar 2024

How would you design a resource allocation app?

I built this system for a real problem: an ISP needed to connect new 10Gbps internet customers to the nearest available infrastructure chamber. The sales team needed to enter customer details, find the closest chamber with capacity, and get alerts when capacity was running low.

It sounds like a simple lookup. It's actually a geospatial resource allocation problem with capacity constraints. And I used CQRS to keep it clean.

The Problem

Screenshot 2024-03-24 at 10.22.06 AM.png

A sales team member enters details about a new customer ordering a 10Gbps internet product. The system needs to:

  1. Persist the new customer's details.
  2. Find the closest chamber with available capacity and return its ID.
  3. Alert the user if the nearest chamber is full — but still show the next closest one that has capacity.
  4. Alert the user if the chamber they're assigned to is now at capacity (meaning the next customer will need a different one).

Screenshot 2024-03-24 at 10.22.06 AM.png

High-Level Architecture

I used CQRS (Command Query Responsibility Segregation) because the read and write patterns are fundamentally different. Writes are infrequent (new customer orders). Reads are frequent and complex (geospatial queries, capacity checks, dashboard views).

architecture-high-level.png

API Design

The API breaks down by domain:

  • Auth — Login, Register, Forgot Password, Reset Password, Resend Verification
  • Package — List available packages
  • Chamber — Get available chambers per package (geospatial query with capacity filter)
  • Order — Create, Get All By User, Change, Get Orders By User
  • User — Get Profile, Get All Customers Created By Me, Create

Two roles: sales and customer. Sales users create orders on behalf of customers.

UI Layout and User Flow

ui-sketch.png

The flow is intentionally simple. The sales team doesn't want complexity — they want to enter a customer's location, see the nearest available chamber, and confirm the order.

Key Implementation Details

Seeding Geospatial Data with Prisma

The chambers have lat/lng coordinates and PostGIS geometry columns for efficient spatial queries:

Text
await prisma.$executeRaw`
    INSERT INTO "Chamber" (id, latitude, longitude, total_capacity, used_capacity, geom)
    VALUES
    (
        'VORB-X8734',
        51.52466903333144,
        -0.08320212364196779,
        100,
        70,
        ST_SetSRID(ST_MakePoint(-0.08320212364196779, 51.52466903333144), 4326)
    ),
    (
        'VORB-Z4784',
        51.523641015718525,
        -0.08601307868957521,
        100,
        10,
        ST_SetSRID(ST_MakePoint(-0.08601307868957521, 51.523641015718525), 4326)
    ),
    (
        'VORB-N2837',
        51.523434943212514,
        -0.08114755153656007,
        100,
        40,
        ST_SetSRID(ST_MakePoint(-0.08114755153656007, 51.523434943212514), 4326)
    ),
    (
        'VORB-V9345',
        51.52211691871454,
        -0.0851869583129883,
        100,
        30,
        ST_SetSRID(ST_MakePoint(-0.0851869583129883, 51.52211691871454), 4326)
    ),
    (
        'VORB-Q9547',
        51.523304662537235,
        -0.08331477642059326,
        100,
        70,
        ST_SetSRID(ST_MakePoint(-0.08331477642059326, 51.523304662537235), 4326)
    );
`;

Querying the Closest Chamber

PostgreSQL with PostGIS handles geospatial distance queries natively. This is the core query — find the closest chamber to a given coordinate:

Text
const chambers = await pg.query( `
    SELECT * FROM "Chamber"
    ${ids?.length ? `WHERE id IN (${ids.join(",")})` : ""}
    ORDER BY ST_Distance(
      geom,
      ST_SetSRID(ST_MakePoint($1, $2), 4326)
    )
    LIMIT $3;
  `,
    [longitude, latitude, limit || 1]
  );

ST_Distance calculates the distance between the customer's location and each chamber. ORDER BY sorts by nearest. LIMIT returns the top N results. Simple, fast, and handled entirely by the database.

Backend Dockerfile

Text
FROM node:lts-iron as build
RUN apt-get update -y
RUN apt-get install -y openssl
RUN apt-get install -y apt-utils

ARG DATABASE_URL=""
ARG NODE_ENV=production
ARG PORT=8000
ARG JWT_SECRET=""

ENV NODE_ENV=$NODE_ENV
ENV PORT=$PORT
ENV DATABASE_URL=$DATABASE_URL
ENV JWT_SECRET=$JWT_SECRET

WORKDIR /app
COPY package*.json ./
RUN npm install 
RUN npm install --only=dev

COPY . .
RUN npx prisma generate

EXPOSE 8000
CMD ["npm", "start"]

Database with Docker Compose

PostGIS extends PostgreSQL with geospatial types and functions:

Text
version: "3.8"
services:
  db:
    image: postgis/postgis
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    restart: unless-stopped

Frontend with Remix and Tailwind

Text
FROM node:lts-iron as build
RUN apt-get update -y
RUN apt-get install -y openssl
RUN apt-get install -y apt-utils

ARG NODE_ENV=production
ARG PORT=3000
ARG REACT_APP_API_URL=http://localhost:8000

WORKDIR /app
ENV NODE_ENV=development
COPY package*.json ./
RUN npm install 

COPY . .
RUN npm run build
ENV NODE_ENV=production
ENV PORT=$PORT
ENV REACT_APP_API_URL=$REACT_APP_API_URL

EXPOSE 3000
CMD ["npm", "start"]

Full Stack Docker Compose

One command to run everything:

Text
version: '3.8'
services:  
  database: 
    container_name: app-database
    image: postgis/postgis
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    restart: unless-stopped
    volumes:
      - database-data:/var/lib/postgresql/data

  backend:
    container_name: app-backend
    build: 
      context: ./backend
      dockerfile: Dockerfile
      args:
        NODE_ENV: development
        DATABASE_URL: postgres://postgres:postgres@database:5432/mydb?schema=public&connect_timeout=300
        JWT_SECRET: randomsecret
        PORT: 8000
    ports:
      - "8000:8000"
    depends_on:
      - database
    links:
      - database
    restart: unless-stopped

  frontend:
    container_name: app-frontend
    build: 
      context: ./frontend
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
        PORT: 3000
        REACT_APP_API_URL: http://backend:8000
    depends_on:
      - backend
    links:
      - backend
    ports:
      - "3000:3000"
    restart: unless-stopped
volumes:
  database-data:

The Trade-Offs

PostGIS vs. application-level distance calculation. PostGIS handles geospatial math in the database, which is fast and correct (it accounts for Earth's curvature). Calculating distances in application code is error-prone and doesn't scale — you'd need to load all chambers into memory. PostGIS is the right tool here.

CQRS vs. simple CRUD. CQRS adds complexity — separate read and write models, eventual consistency concerns. But for this system, reads (geospatial queries with capacity filters) and writes (new customer orders) have fundamentally different optimization needs. CQRS made the read path fast without complicating the write path.

Capacity management: optimistic vs. pessimistic locking. Two sales reps assigning the last capacity slot simultaneously is a real race condition. I used pessimistic locking (database-level locks on capacity updates) because the cost of over-allocation is high — it means promising a customer service you can't deliver.