Monolith vs Microservices: Choosing the Right Backend Architecture
One of the most critical decisions in backend architecture is whether to build a monolith or microservices. There's no universal right answer - only the right answer for your context.
Monolithic Architecture
What is a Monolith?
A single codebase where all features are tightly integrated:
┌─────────────────────────────────────┐
│ Monolithic App │
├─────────────────────────────────────┤
│ User Module │ Product Module │
│ Order Module │ Payment Module │
│ Auth Module │ Notification Module│
├─────────────────────────────────────┤
│ Shared Database │
└─────────────────────────────────────┘
Directory Structure
monolith-app/
├── src/
│ ├── modules/
│ │ ├── auth/
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ └── auth.model.ts
│ │ ├── products/
│ │ ├── orders/
│ │ └── payments/
│ ├── middleware/
│ ├── utils/
│ └── app.ts
├── database/
│ └── schema.prisma
└── package.json
Implementation Example
// NestJS Monolithic Application
import { Module } from "@nestjs/common";
import { AuthModule } from "./modules/auth/auth.module";
import { ProductModule } from "./modules/products/product.module";
import { OrderModule } from "./modules/orders/order.module";
@Module({
imports: [AuthModule, ProductModule, OrderModule],
})
export class AppModule {}
// Single database for all modules
@Injectable()
export class DatabaseService {
private prisma = new PrismaClient();
async getUser(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}
async getProducts() {
return this.prisma.product.findMany();
}
async createOrder(data: CreateOrderDTO) {
return this.prisma.order.create({ data });
}
}Advantages of Monolith
✅ Simplicity: Single codebase, unified deployment
# One command to deploy everything
git push
# All features deployed together✅ Performance: No network overhead between components
// Direct function calls - no HTTP
const user = await userService.getUser(id);
const orders = await orderService.getOrders(user.id);
// Millisecond latency✅ Data Consistency: ACID transactions across features
// Atomic transaction
await db.$transaction(async (tx) => {
await tx.inventory.update({ ... });
await tx.order.create({ ... });
await tx.payment.update({ ... });
// All succeed or all rollback
});✅ Easier Testing: Integration tests are simpler
// One test suite covers entire flow
describe("Order Flow", () => {
it("should process complete order", async () => {
const user = await createTestUser();
const product = await createTestProduct();
const order = await orderService.createOrder(user.id, product.id);
expect(order.status).toBe("confirmed");
});
});✅ Cost Effective: Single deployment, lower infrastructure costs
Small monolith: 1 server = $50/month
Same as 3-5 microservices: $200-500/month
Disadvantages of Monolith
❌ Scalability Limits: Can't scale individual features
100 concurrent users browsing products
50 concurrent users processing orders
Both hit same database bottleneck
❌ Technology Lock-in: Entire app uses same tech stack
// Stuck with chosen framework
// Want to use Python for ML? Can't easily integrate
// Want Go for performance? Rewrite required❌ Deployment Risk: One bug affects everything
Bug in email service → entire app deployment blocked
Feature X ready for release → must wait for unrelated fixes
❌ Team Scalability: Multiple teams working on same codebase
Team conflicts:
- Shared database schema changes
- Shared API contract changes
- Merge conflicts in shared code
Microservices Architecture
What are Microservices?
Independent services, each with specific responsibility:
┌─────────────────┐ ┌──────────────┐ ┌─────────────┐
│ User Service │ │Product Service│ │Auth Service │
├─────────────────┤ ├──────────────┤ ├─────────────┤
│User Database │ │Product DB │ │Auth DB │
└────────┬────────┘ └───────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼────────────────┘
API Gateway
│
Client App
Implementation Example
// User Service
@Controller("api/users")
export class UserController {
@Get(":id")
async getUser(@Param("id") id: number) {
return this.userService.findById(id);
}
}
// Product Service
@Controller("api/products")
export class ProductController {
@Get()
async getProducts() {
return this.productService.findAll();
}
}
// API Gateway (routes requests to services)
async function gateway(req, res) {
if (req.path.startsWith("/api/users")) {
return forwardToService("http://user-service:3001", req);
}
if (req.path.startsWith("/api/products")) {
return forwardToService("http://product-service:3002", req);
}
}Inter-Service Communication
// Synchronous (HTTP/gRPC)
class OrderService {
async createOrder(userId: number, productId: number) {
// Call User Service
const user = await this.http.get(`http://user-service/users/${userId}`);
// Call Product Service
const product = await this.http.get(
`http://product-service/products/${productId}`,
);
return { user, product, orderedAt: new Date() };
}
}
// Asynchronous (Message Queue)
class OrderService {
async createOrder(data: CreateOrderDTO) {
const order = await this.db.order.create({ data });
// Publish event - other services react asynchronously
await this.queue.publish("order.created", {
orderId: order.id,
userId: order.userId,
});
return order;
}
}
// Payment Service subscribes to order.created
class PaymentService {
constructor(private queue: MessageQueue) {
this.queue.subscribe("order.created", this.handleOrderCreated);
}
private async handleOrderCreated(event) {
// Process payment asynchronously
const payment = await this.processPayment(event.orderId);
// Publish payment.processed event
}
}Advantages of Microservices
✅ Independent Scaling: Scale only what you need
Traffic spike for products → scale product service
Normal traffic for users → keep user service as is
✅ Technology Flexibility: Different services, different tech
User Service: Node.js + TypeScript
Product Service: Python + FastAPI
Search Service: Go + Elasticsearch
✅ Deployment Independence: Deploy services separately
# Deploy only user service without affecting others
deploy user-service v2.0
# Orders still processing on current version✅ Team Independence: Different teams own services
Backend Team A owns User Service
Backend Team B owns Order Service
Zero merge conflicts, independent deployment
✅ Resilience: Failure isolation
Payment service down?
→ Orders can still be created
→ Payments processed when service recovers
→ App gracefully degrades
Disadvantages of Microservices
❌ Complexity: Distributed systems are hard
// Transaction spanning multiple services
async function refundOrder(orderId: number) {
const order = await orderService.getOrder(orderId);
const refunded = await paymentService.refund(order.paymentId);
const updated = await orderService.updateStatus(orderId, "refunded");
// What if paymentService succeeds but orderService fails?
// Inconsistent state - need complex compensation logic
}❌ Network Latency: Service-to-service calls are slow
Monolith: Direct function call = 1ms
Microservices: HTTP call = 10-50ms
10 service calls per request = 100-500ms added latency
❌ Data Consistency: No ACID transactions across services
// SAGA Pattern for distributed transactions
class OrderSaga {
async createOrder(data) {
// Step 1: Create order
const order = await orderService.create(data);
// Step 2: Reserve inventory
const reserved = await inventoryService.reserve(data.productId);
if (!reserved) {
// Compensate: cancel order
await orderService.cancel(order.id);
throw new Error("Insufficient inventory");
}
// Step 3: Process payment
try {
await paymentService.charge(data.amount);
} catch (error) {
// Compensate both previous steps
await inventoryService.release(data.productId);
await orderService.cancel(order.id);
throw error;
}
}
}❌ Infrastructure Costs: Multiple services to manage
Monolith: 1 server = $50/month
Microservices: 5 services × 2 replicas = $500/month
❌ Debugging Difficulty: Distributed tracing required
// Must trace request across multiple services
const traceId = generateTraceId();
// Pass traceId to each service
userService.getUser(id, traceId);
orderService.getOrders(id, traceId);
// Can now correlate logs across servicesComparison Table
| Aspect | Monolith | Microservices |
|---|---|---|
| Complexity | Low | High |
| Performance | Fast | Slower |
| Scalability | Limited | Unlimited |
| Deployment | Together | Independent |
| Testing | Simple | Complex |
| Cost | Low | High |
| Team Coordination | Central | Distributed |
| Data Consistency | ACID | Eventual |
| Development Speed | Fast | Slower |
Decision Framework
Choose Monolith if:
- ✅ < 10 developers on project
- ✅ Single feature set
- ✅ Same technology stack suitable
- ✅ High data consistency needed
- ✅ MVP or startup phase
- ✅ Budget constrained
Choose Microservices if:
- ✅ 50+ developers across teams
- ✅ Multiple independent features
- ✅ Different scaling needs per feature
- ✅ Technology diversity needed
- ✅ Fault isolation critical
- ✅ Sufficient DevOps budget
Real-World Evolution: Floral Radiance
Our growth journey:
Phase 1 (2024): Monolith
├─ 2 developers
├─ Node.js + Express
├─ Single PostgreSQL
└─ One deployment per week
Phase 2 (2025): Monolith → Strangler Pattern
├─ 5 developers
├─ Extract search service (high load)
├─ Keep everything else monolithic
└─ Gradual migration to microservices
Phase 3 (2026): Hybrid Architecture
├─ 15 developers across 4 teams
├─ Core monolith (User, Order, Payment)
├─ Microservices for specialized needs
│ ├─ Search (Elasticsearch + Go)
│ ├─ Notifications (Node.js + Bull Queue)
│ └─ Analytics (Python + Apache Spark)
└─ API Gateway coordination
Hybrid Approach: Best of Both Worlds
The "Monolith + Microservices" sweet spot:
┌──────────────────────────────────┐
│ Monolithic Core │
│ (User, Order, Auth, Payment) │
└────────────────┬─────────────────┘
│
┌────────┼────────┐
│ │ │
┌───▼─┐ ┌──▼──┐ ┌──▼──┐
│Search│ │Notify│ │Image│
│Service│ │Service│ │Service│
└──────┘ └─────┘ └────┘
Conclusion
The architecture choice isn't binary. It's about:
- Current state: Start with monolith
- Growth patterns: Extract services when needed
- Team structure: Architecture should match team structure (Conway's Law)
- Specific problems: Use microservices to solve specific scaling/technology problems
The best architecture is the simplest one that solves your problems. Build monoliths until you have clear reasons to extract services.