This article is available in Indonesian

๐Ÿ‡ฎ๐Ÿ‡ฉ Baca dalam Bahasa Indonesia

May 26, 2026

๐Ÿ‡ฌ๐Ÿ‡ง English

Fleet Management Part 7: From Monolith to Microservices

Transform a monolith into microservices. Service boundaries, inter-service communication, API gateway, saga pattern, and when NOT to use microservices.

7 min read

What Are Microservices?

Let me explain microservices with a simple analogy.

Monolith = One restaurant where one chef cooks everything โ€” appetizers, main course, desserts, drinks. If the chef gets sick, the whole restaurant closes.

Microservices = A food court with specialized stalls. One stall makes pizza, another makes sushi, another makes coffee. If the pizza stall closes for maintenance, you can still get sushi and coffee.

Monolith: Microservices: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Everything โ”‚ โ”‚Pizza โ”‚ โ”‚Sushi โ”‚ โ”‚Coffeeโ”‚ โ”‚ in one app โ”‚ โ†’ โ”‚Stall โ”‚ โ”‚Stall โ”‚ โ”‚Stall โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ”‚ One database โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ–ผโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ–ผโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ–ผโ”€โ”€โ”€โ” โ”‚ DB 1 โ”‚ โ”‚ DB 2 โ”‚ โ”‚ DB 3 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each microservice:

  • Has its own codebase (can be different languages)
  • Has its own database (can be different database types)
  • Can be deployed independently (deploy pizza without touching sushi)
  • Can scale independently (add more pizza stalls during lunch rush)

When NOT to Use Microservices (A Senior Perspective)

This is the most important section. Most tutorials teach you how to build microservices but never teach you when not to.

The Microservices Decision Matrix

FactorUse MonolithUse Microservices
Team size1-5 developers10+ developers
Domain complexitySimple, related featuresDistinct, independent domains
Deployment frequencySame release cycleDifferent teams, different schedules
Scale requirementsUniform loadSome parts need 10x more resources
OrganizationOne teamMultiple autonomous teams

Why We Chose a Hybrid (3 Services, Not 30)

Our fleet management system has three clearly separate domains:

  1. Fleet API (NestJS) โ€” Handles fleet CRUD, authentication, route planning
  2. Admin Service (Laravel) โ€” Internal admin panel, invoicing, scheduling
  3. Telemetry Service (NestJS) โ€” GPS data ingestion, fuel monitoring, alerts

These are separate because:

  • Different update frequencies โ€” Telemetry changes weekly, admin panel changes monthly
  • Different scaling needs โ€” Telemetry processes 1000+ messages/minute, admin handles 10 users
  • Different tech requirements โ€” Telemetry needs PostgreSQL + Redis, admin needs MySQL + Filament

What I would NOT split:

  • Don't separate "User Service" from "Auth Service" โ€” they change together
  • Don't separate "Vehicle Service" from "Fleet Service" โ€” same domain
  • Don't create a "Notification Service" with only 3 endpoints

Service Boundaries: How to Split

The hardest part of microservices is deciding where to draw the lines. Here's the process I follow:

Step 1: Identify Bounded Contexts (Domain-Driven Design)

Fleet Management System โ”œโ”€โ”€ Fleet Context โ†’ Fleet API Service โ”‚ โ”œโ”€โ”€ Vehicles โ”‚ โ”œโ”€โ”€ Drivers โ”‚ โ”œโ”€โ”€ Routes โ”‚ โ””โ”€โ”€ Authentication โ”‚ โ”œโ”€โ”€ Administration Context โ†’ Admin Service โ”‚ โ”œโ”€โ”€ Invoicing โ”‚ โ”œโ”€โ”€ Scheduling โ”‚ โ”œโ”€โ”€ Reporting โ”‚ โ””โ”€โ”€ User Management โ”‚ โ””โ”€โ”€ Telemetry Context โ†’ Telemetry Service โ”œโ”€โ”€ GPS Tracking โ”œโ”€โ”€ Fuel Monitoring โ””โ”€โ”€ Alert System

Step 2: Define the API Contracts

Each service exposes a clear API. Other services interact only through these APIs โ€” never directly accessing each other's databases.

// Fleet API โ€” exposed to frontend and other services
POST   /api/vehicles         // Create vehicle
GET    /api/vehicles         // List vehicles
GET    /api/vehicles/:id     // Get vehicle details
PATCH  /api/vehicles/:id     // Update vehicle

// Telemetry Service โ€” internal API
POST   /api/telemetry/gps    // Receive GPS data (from truck devices)
GET    /api/telemetry/position/:truckId  // Get latest position
POST   /api/telemetry/alert  // Create alert

// Admin Service โ€” internal admin panel
GET    /admin/dashboard      // Admin dashboard
GET    /admin/invoices       // Invoice management

Inter-Service Communication

Services need to talk to each other. There are two main patterns:

1. Synchronous (REST/gRPC) โ€” Request-Response

Fleet API โ”€โ”€โ”€โ”€ HTTP GET โ”€โ”€โ”€โ”€โ†’ Telemetry Service โ†โ”€โ”€ JSON Response โ”€โ”€
// Fleet API calling Telemetry Service synchronously
@Injectable()
export class TelemetryClient {
  constructor(private readonly httpService: HttpService) {}

  async getVehiclePosition(truckId: string): Promise<Position> {
    const { data } = await this.httpService.axiosRef.get(
      `${TELEMETRY_URL}/api/telemetry/position/${truckId}`,
      { timeout: 3000 }, // Always set timeouts!
    );
    return data;
  }
}

Use when: You need an immediate response (e.g., "Where is this truck right now?")

2. Asynchronous (Message Queue) โ€” Event-Driven

Telemetry Service โ”€โ”€ publish event โ”€โ”€โ†’ Redis Pub/Sub โ”‚ Fleet API โ†โ”€โ”€ receives event โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Admin Service โ†โ”€โ”€ receives event โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Telemetry Service publishes an event
async function onGpsReceived(data: GpsData) {
  // Save to database
  await this.gpsRepo.save(data);

  // Publish event โ€” anyone listening can react
  await this.redis.publish('telemetry:gps-update', JSON.stringify({
    truckId: data.truckId,
    lat: data.latitude,
    lng: data.longitude,
    speed: data.speed,
    timestamp: data.recordedAt,
  }));
}

// Fleet API subscribes to the event
this.redis.subscribe('telemetry:gps-update', (message) => {
  const data = JSON.parse(message);
  // Update the real-time dashboard via WebSocket
  this.wsGateway.broadcastPosition(data);
});

Use when: The sender doesn't need to wait for a response (e.g., "A truck moved โ€” update all dashboards")


The API Gateway Pattern

Instead of the frontend calling 3 different services, we put an API Gateway in front:

Frontend โ”€โ”€โ†’ Nginx (API Gateway) โ”€โ”€โ†’ Fleet API (:3001) โ”€โ”€โ†’ Telemetry (:3002) โ”€โ”€โ†’ Admin (:8000)
# nginx.conf โ€” Simple API Gateway
upstream fleet_api {
    server 127.0.0.1:3001;
}

upstream telemetry_api {
    server 127.0.0.1:3002;
}

upstream admin_api {
    server 127.0.0.1:8000;
}

server {
    listen 80;

    # Route by URL prefix
    location /api/vehicles {
        proxy_pass http://fleet_api;
    }

    location /api/telemetry {
        proxy_pass http://telemetry_api;
    }

    location /admin {
        proxy_pass http://admin_api;
    }
}

Benefits:

  • Frontend only knows one URL
  • SSL termination in one place
  • Rate limiting applied uniformly
  • Easy to add/remove services

The Saga Pattern: Distributed Transactions

Here's the tricky part of microservices: transactions that span multiple services.

Example: "Assign driver to delivery" touches Fleet API (update vehicle) AND Admin Service (create invoice). What if the invoice creation fails?

The Choreography Saga

1. Fleet API: Assign driver to vehicle โœ… โ†’ Publishes "driver.assigned" event 2. Admin Service: Receives event, creates invoice โŒ FAILS! โ†’ Publishes "invoice.failed" event 3. Fleet API: Receives "invoice.failed", rolls back driver assignment โ†’ Publishes "driver.unassigned" event
// Fleet API โ€” Step 1: Assign driver
async assignDriver(vehicleId: string, driverId: string) {
  const vehicle = await this.vehicleRepo.findOneOrFail(vehicleId);
  vehicle.currentDriverId = driverId;
  await this.vehicleRepo.save(vehicle);

  // Publish event
  await this.eventBus.publish('driver.assigned', {
    vehicleId, driverId, timestamp: new Date(),
  });
}

// Fleet API โ€” Step 3: Compensate on failure
@OnEvent('invoice.failed')
async handleInvoiceFailed(event: InvoiceFailedEvent) {
  // Roll back the driver assignment
  const vehicle = await this.vehicleRepo.findOneOrFail(event.vehicleId);
  vehicle.currentDriverId = null;
  await this.vehicleRepo.save(vehicle);

  this.logger.warn(`Rolled back driver assignment for vehicle ${event.vehicleId}`);
}

Circuit Breaker: Handling Service Failures

What happens when the Telemetry Service is down? Without a circuit breaker, the Fleet API keeps sending requests and timing out โ€” making it slow for users.

// Simple circuit breaker implementation
class CircuitBreaker {
  private failures = 0;
  private lastFailure: number = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';

  constructor(
    private readonly threshold: number = 5,     // Open after 5 failures
    private readonly resetTimeout: number = 30000, // Try again after 30s
  ) {}

  async call<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'HALF_OPEN'; // Try one request
      } else {
        return fallback; // Return fallback immediately
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      return fallback;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  private onFailure() {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

// Usage
const telemetryBreaker = new CircuitBreaker(5, 30000);

async function getVehiclePosition(truckId: string) {
  return telemetryBreaker.call(
    () => telemetryClient.getPosition(truckId),
    { lat: 0, lng: 0, status: 'unavailable' } // Fallback
  );
}

Common Microservices Mistakes

Mistake 1: Distributed Monolith

If every service change requires deploying all other services, you have a distributed monolith โ€” all the complexity of microservices with none of the benefits.

Mistake 2: Shared Database

If two services read/write to the same database tables, they're not really separate services. Each service owns its data.

Mistake 3: Too Many Services Too Early

Start with 2-3 well-defined services. You can always split later. Merging services back together is much harder.


What's Next

In Part 8, we'll cover DevSecOps โ€” setting up CI/CD pipelines with GitHub Actions, security scanning, Docker containerization, and production deployment. The final piece of the puzzle.


This is Part 7 of the Fleet Management System series. Understanding microservices isn't about knowing the patterns โ€” it's about knowing when to apply them and when to resist the urge.

Continue Reading

Previous article

โ† Previous Article

Fleet Management Part 6: SOLID Principles & Design Patterns in Practice

Next Article โ†’

Fleet Management Part 8: DevSecOps, CI/CD & Production Deployment

Next article