Monolith to Microservices: Building an Application

Introduction

I built a Formula 1 application to manage Teams, Tracks, Drivers, and Races, starting as a monolithic Next.js + Payload CMS app. As my needs grew, I transitioned to a microservices architecture for better scalability, modularity, and autonomy. This article details my process—folder structure, code changes, challenges, and deployment—using my F1 app as an example.

The Monolith: Initial Setup

I began with a monolithic app using create-payload-app, combining Next.js (frontend) and Payload CMS (backend) in a single codebase. It managed Teams, Tracks, Drivers, and Races, with a Dashboard for analytics, and was containerized with Docker for client-specific deployments. Client access was controlled via NEXT_PUBLIC_ENABLED_ROUTES (e.g., Client 1: Dashboard, Tracks; Client 2: Teams, Tracks).

Initial Folder Structure

f1-app/
├── src/
│   ├── app/
│   │   ├── (frontend)/        # Next.js UI
│   │   │   ├── components/    # Reusable UI components
│   │   │   ├── drivers/       # Drivers pages
│   │   │   ├── races/         # Races pages
│   │   │   ├── teams/         # Teams pages
│   │   │   └── tracks/        # Tracks pages
│   │   ├── (payload)/         # Payload CMS
│   │   │   ├── admin/         # Admin panel
│   │   │   └── api/           # API endpoints
│   │   └── public/            # Static assets
├── collections/               # Payload collections (Teams, Tracks, Drivers, Races)
├── docker-compose.yml         # Docker orchestration
├── Dockerfile                 # Monolith container
├── package.json              # Dependencies
└── tsconfig.json             # TypeScript config

Monolith Pros and Cons

  • Pros: Fast development, simple deployment (single container), unified codebase.
  • Cons: Tight coupling, scalability issues, slow builds as the app grew.

Monolith Docker Setup

I used a single docker-compose.yml for a Next.js/Payload container and Postgres database

Transition to Microservices

To address scalability and modularity, I extracted Teams and Drivers into standalone Payload CMS microservices, keeping Next.js as the frontend. Each microservice got its own codebase, database schema, and container.

Step 1: Microservices Folder Structure

I restructured the project to support microservices:

f1-app/
├── services/
│   ├── teams/
│   │   ├── src/
│   │   │   ├── collections/    # Teams collection
│   │   │   ├── payload.config.ts # Payload config
│   │   ├── Dockerfile          # Teams service container
│   │   ├── package.json        # Service dependencies
│   │   └── tsconfig.json       # TypeScript config
│   ├── drivers/
│   │   ├── src/
│   │   │   ├── collections/    # Drivers collection
│   │   │   ├── payload.config.ts
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── tsconfig.json
├── frontend/
│   ├── src/
│   │   ├── app/
│   │   │   ├── (frontend)/     # Next.js UI
│   │   │   ├── components/
│   │   │   ├── drivers/
│   │   │   ├── races/
│   │   │   ├── teams/
│   │   │   └── tracks/
│   │   └── public/
│   ├── package.json
│   ├── tsconfig.json
│   └── Dockerfile
├── docker-compose.yml          # Orchestrates all services
└── .env                        # Environment variables

Step 2: Dockerizing Microservices

Each service was containerized with its own Dockerfile:

# services/teams/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3001
CMD ["npm", "run", "start"]

I updated docker-compose.yml to orchestrate services and a shared Postgres database:

version: '3.8'
services:
  teams-service:
    build: ./services/teams
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/teams_db
  drivers-service:
    build: ./services/drivers
    ports:
      - "3002:3002"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/drivers_db
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - NEXT_PUBLIC_ENABLED_ROUTES=dashboard,teams,tracks,drivers,races
      - NEXT_PUBLIC_TEAMS_API=http://teams-service:3001/api
      - NEXT_PUBLIC_DRIVERS_API=http://drivers-service:3002/api
  db:
    image: postgres:15
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_MULTIPLE_DATABASES=teams_db,drivers_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
volumes:
  postgres_data:

Step 3: Updating the Next.js Frontend

I updated the frontend to fetch data from microservices:

// frontend/src/app/teams/page.tsx
async function getTeams() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_TEAMS_API}/teams`, { cache: 'no-store' });
  return res.json();
}
export default async function TeamsPage() {
  const teams = await getTeams();
  return (
    <div>
      <h1>Teams</h1>
      <ul>
        {teams.docs.map((team: any) => (
          <li key={team.id}>{team.name}</li>
        ))}
      </ul>
    </div>
  );
}

Step 5: Deployment

I deployed using Docker Compose for development and plan to use Kubernetes for production:

  • Build and Test Locally: docker-compose up --build
  • Production Deployment: Push images to a registry (e.g., Docker Hub), use Kubernetes for orchestration, scaling, and load balancing, and set up CI/CD with GitHub Actions for automated builds and deployments.
  • Environment Variables: Stored in .env for local dev, managed via Kubernetes secrets in production.

Conclusion

The migration from a monolithic architecture to microservices for the F1 application demonstrates the transformative potential of modular design in modern software development. By decomposing a tightly coupled system into independent, scalable services, the application achieved greater flexibility, improved maintainability, and the ability to scale components based on demand. While the transition introduced new challenges such as inter-service communication, latency, and deployment complexity the benefits of modularity, team autonomy, and scalability far outweigh the initial overhead.

Leave a comment

Your email address will not be published. Required fields are marked *