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
| Factor | Use Monolith | Use Microservices |
|---|---|---|
| Team size | 1-5 developers | 10+ developers |
| Domain complexity | Simple, related features | Distinct, independent domains |
| Deployment frequency | Same release cycle | Different teams, different schedules |
| Scale requirements | Uniform load | Some parts need 10x more resources |
| Organization | One team | Multiple autonomous teams |
Why We Chose a Hybrid (3 Services, Not 30)
Our fleet management system has three clearly separate domains:
- Fleet API (NestJS) โ Handles fleet CRUD, authentication, route planning
- Admin Service (Laravel) โ Internal admin panel, invoicing, scheduling
- 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.
