Skip to main content

Microservices Foundations

Before diving into implementation, it’s crucial to understand when and why to use microservices. Many teams adopt microservices for the wrong reasons and end up with a distributed monolith that’s harder to maintain than what they started with.
Learning Objectives:
  • Understand the evolution from monolith to microservices
  • Learn when to choose microservices vs monolith
  • Master key microservices principles
  • Identify common anti-patterns to avoid

What Are Microservices?

Microservices architecture is a software design approach where an application is built as a collection of small, independent services that:
  1. Run in their own process - Each service is deployed independently
  2. Communicate via lightweight protocols - Usually HTTP/REST or messaging
  3. Are organized around business capabilities - Not technical layers
  4. Can be deployed independently - Without affecting other services
  5. May use different technologies - Best tool for each job
┌────────────────────────────────────────────────────────────────────────────┐
│                         MONOLITHIC APPLICATION                              │
│  ┌──────────────────────────────────────────────────────────────────────┐  │
│  │                         Single Deployable Unit                        │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐             │  │
│  │  │   User   │  │  Order   │  │ Inventory│  │ Payment  │             │  │
│  │  │  Module  │  │  Module  │  │  Module  │  │  Module  │             │  │
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘             │  │
│  │       └─────────────┴──────────────┴──────────────┘                  │  │
│  │                          SHARED DATABASE                              │  │
│  │                     ┌──────────────────────┐                         │  │
│  │                     │      PostgreSQL      │                         │  │
│  │                     └──────────────────────┘                         │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────┘



┌────────────────────────────────────────────────────────────────────────────┐
│                       MICROSERVICES ARCHITECTURE                            │
│                                                                             │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                 │
│  │ User Service │    │Order Service │    │Inventory Svc │                 │
│  │   ┌──────┐   │    │   ┌──────┐   │    │   ┌──────┐   │                 │
│  │   │ API  │   │    │   │ API  │   │    │   │ API  │   │                 │
│  │   └──────┘   │    │   └──────┘   │    │   └──────┘   │                 │
│  │   ┌──────┐   │    │   ┌──────┐   │    │   ┌──────┐   │                 │
│  │   │ DB   │   │    │   │ DB   │   │    │   │ DB   │   │                 │
│  │   └──────┘   │    │   └──────┘   │    │   └──────┘   │                 │
│  └──────────────┘    └──────────────┘    └──────────────┘                 │
│       MongoDB              PostgreSQL           Redis                      │
│                                                                             │
└────────────────────────────────────────────────────────────────────────────┘

Monolith vs Microservices

The Monolith

A monolithic application is a single deployable unit containing all business functionality.
✅ Simplicity
  • Single codebase, easier to understand
  • Simple deployment (one artifact)
  • Easy local development
  • Straightforward debugging
  • ACID transactions across all data
✅ Performance
  • No network latency between modules
  • Shared memory, faster communication
  • No serialization/deserialization overhead
✅ Development
  • Easier to refactor
  • Simple IDE navigation
  • Consistent tech stack
  • Unified testing
// Monolithic Express Application
const express = require('express');
const app = express();

// All modules in one application
const userRoutes = require('./routes/users');
const orderRoutes = require('./routes/orders');
const inventoryRoutes = require('./routes/inventory');
const paymentRoutes = require('./routes/payments');
const notificationRoutes = require('./routes/notifications');

// Single database connection
const db = require('./database');

app.use('/api/users', userRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/inventory', inventoryRoutes);
app.use('/api/payments', paymentRoutes);
app.use('/api/notifications', notificationRoutes);

// One process, one deployment
app.listen(3000);

Microservices

✅ Independent Deployment
  • Deploy services independently
  • Faster release cycles
  • Lower risk deployments
  • Zero-downtime updates
✅ Scalability
  • Scale services independently
  • Optimize resources per service
  • Handle varying loads efficiently
✅ Team Autonomy
  • Clear ownership
  • Independent technology choices
  • Parallel development
  • Easier onboarding (smaller scope)
✅ Resilience
  • Fault isolation
  • Graceful degradation
  • Independent failure recovery

When to Use Microservices

✅ Use Microservices When:

Large Team

50+ developers working on the same product. Team autonomy becomes crucial.

Different Scaling Needs

Some features need 10x more resources than others (e.g., search vs profile).

Technology Diversity

Different problems need different tools (ML in Python, real-time in Go).

High Availability

99.99% uptime requirements where partial degradation is acceptable.

Clear Domain Boundaries

Well-defined business domains with clear interfaces.

Frequent Deployments

Need to deploy multiple times per day to different components.

❌ Don’t Use Microservices When:

Small Team

Fewer than 10 developers. Overhead isn’t worth it.

Startup MVP

Still figuring out the product. Need flexibility to pivot.

Simple CRUD

Basic create/read/update/delete without complex business logic.

Limited DevOps

No infrastructure automation. Manual deployments become nightmare.

Unclear Boundaries

Can’t identify clear service boundaries. Will create distributed monolith.

Strong Consistency Needed

Transactions must span multiple domains atomically.

The Microservices Decision Matrix

Use this matrix to evaluate if microservices are right for your situation:
FactorScore 1-5Weight
Team Size___3x
Domain Clarity___3x
Scaling Variance___2x
Deployment Frequency___2x
Technology Diversity Need___1x
DevOps Maturity___3x
Total Score___/70
Interpretation:
  • < 35: Stick with monolith
  • 35-50: Consider modular monolith
  • > 50: Microservices may be beneficial

Key Microservices Principles

1. Single Responsibility

Each service should do one thing well and have a clear, bounded responsibility.
✅ GOOD Service Boundaries:
┌──────────────────┬──────────────────┬──────────────────┐
│   User Service   │   Order Service  │ Payment Service  │
├──────────────────┼──────────────────┼──────────────────┤
│ • Registration   │ • Cart mgmt      │ • Process payment│
│ • Authentication │ • Order creation │ • Refunds        │
│ • Profile mgmt   │ • Order history  │ • Payment history│
│ • Preferences    │ • Order status   │ • Payment methods│
└──────────────────┴──────────────────┴──────────────────┘

❌ BAD Service Boundaries:
┌──────────────────────────────────────────────────────────┐
│              User and Order Service                       │
│  • Users + Orders + Payments (too coupled)                │
└──────────────────────────────────────────────────────────┘

2. Loose Coupling

Services should minimize dependencies on other services.
// ❌ Tight Coupling - Direct database access
class OrderService {
  async createOrder(orderData) {
    // Directly accessing user database - BAD!
    const user = await userDatabase.users.findById(orderData.userId);
    // Directly accessing inventory database - BAD!
    const stock = await inventoryDatabase.products.findById(orderData.productId);
    
    if (!user || stock.quantity < orderData.quantity) {
      throw new Error('Invalid order');
    }
    // ... create order
  }
}

// ✅ Loose Coupling - API calls
class OrderService {
  constructor(userClient, inventoryClient) {
    this.userClient = userClient;
    this.inventoryClient = inventoryClient;
  }

  async createOrder(orderData) {
    // Call User Service API
    const user = await this.userClient.getUser(orderData.userId);
    // Call Inventory Service API
    const available = await this.inventoryClient.checkStock(
      orderData.productId, 
      orderData.quantity
    );
    
    if (!user || !available) {
      throw new Error('Invalid order');
    }
    // ... create order
  }
}

3. High Cohesion

Related functionality should be grouped together within a service.
// ✅ High Cohesion - User Service handles all user-related operations
class UserService {
  // All user-related operations in one service
  async createUser(userData) { /* ... */ }
  async getUser(userId) { /* ... */ }
  async updateUser(userId, updates) { /* ... */ }
  async deleteUser(userId) { /* ... */ }
  async authenticateUser(credentials) { /* ... */ }
  async resetPassword(email) { /* ... */ }
  async updatePreferences(userId, prefs) { /* ... */ }
}

// ❌ Low Cohesion - Mixed responsibilities
class MixedService {
  async createUser(userData) { /* ... */ }
  async createOrder(orderData) { /* ... */ }  // Should be in Order Service
  async processPayment(paymentData) { /* ... */ }  // Should be in Payment Service
}

4. Database Per Service

Each service should own its data and expose it only through APIs.
// User Service - MongoDB
const userDb = mongoose.connect(process.env.USER_DB_URI);

const UserSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  name: String,
  passwordHash: String,
  preferences: Object
});

// Order Service - PostgreSQL
const orderDb = new Pool({
  connectionString: process.env.ORDER_DB_URI
});

// Inventory Service - Redis for fast access
const inventoryDb = redis.createClient({
  url: process.env.INVENTORY_REDIS_URI
});

// Each service uses the best database for its needs

5. Design for Failure

Assume other services will fail and design accordingly.
const CircuitBreaker = require('opossum');

class ResilientUserClient {
  constructor() {
    this.breaker = new CircuitBreaker(
      async (userId) => {
        const response = await axios.get(`${USER_SERVICE_URL}/users/${userId}`);
        return response.data;
      },
      {
        timeout: 3000,
        errorThresholdPercentage: 50,
        resetTimeout: 30000
      }
    );

    // Fallback when circuit is open
    this.breaker.fallback((userId) => ({
      id: userId,
      name: 'Unknown User',
      _fallback: true
    }));
  }

  async getUser(userId) {
    return this.breaker.fire(userId);
  }
}

Anti-Patterns to Avoid

1. Distributed Monolith

Services are deployed separately but still tightly coupled.
❌ Distributed Monolith:
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Service A  │───▶│  Service B  │───▶│  Service C  │
│             │◀───│             │◀───│             │
└──────┬──────┘    └──────┬──────┘    └──────┬──────┘
       │                   │                  │
       └───────────────────┴──────────────────┘
                    SHARED DATABASE
                    
Signs:
• Must deploy all services together
• Shared database between services
• Synchronous chain calls
• Breaking one service breaks everything

2. Nano-Services

Services are too small, creating unnecessary complexity.
❌ Too Granular:
• UserCreationService
• UserUpdateService  
• UserDeletionService
• UserQueryService
• UserValidationService

✅ Right Size:
• UserService (handles all user operations)

3. Wrong Service Boundaries

Services split by technical layers instead of business domains.
❌ Technical Split:
• DatabaseService
• APIService  
• ValidationService
• CacheService

✅ Business Domain Split:
• UserService
• OrderService
• PaymentService
• InventoryService

4. Shared Libraries Nightmare

Too much shared code creates hidden coupling.
// ❌ Shared library that's too large
import { 
  User, Order, Payment, Inventory,
  validateUser, validateOrder,
  formatCurrency, calculateTax,
  sendEmail, sendSMS
} from '@company/mega-shared-lib';

// ✅ Minimal, focused shared code
import { Logger } from '@company/logger';
import { HttpClient } from '@company/http-client';
// Business logic stays in respective services

The Strangler Fig Pattern

The safest way to migrate from monolith to microservices.
Phase 1: Identify Bounded Context
┌────────────────────────────────────────┐
│            MONOLITH                    │
│  ┌────────────────────────────────┐    │
│  │          ALL FEATURES          │    │
│  │  Users | Orders | Payments     │    │
│  └────────────────────────────────┘    │
└────────────────────────────────────────┘

Phase 2: Extract One Service
┌────────────────────────────────────────┐
│ ┌──────────────┐                       │
│ │ User Service │ ◀── New Service       │
│ └──────────────┘                       │
│        │                               │
│        ▼                               │
│ ┌────────────────────────────────┐     │
│ │     MONOLITH (reduced)         │     │
│ │  Orders | Payments | Inventory │     │
│ └────────────────────────────────┘     │
└────────────────────────────────────────┘

Phase 3: Continue Extraction
┌────────────────────────────────────────┐
│ ┌──────────────┐  ┌──────────────┐     │
│ │ User Service │  │Order Service │     │
│ └──────────────┘  └──────────────┘     │
│        │                │              │
│        └────────┬───────┘              │
│                 ▼                      │
│ ┌────────────────────────────────┐     │
│ │   MONOLITH (minimal)           │     │
│ │   Payments | Inventory         │     │
│ └────────────────────────────────┘     │
└────────────────────────────────────────┘

Phase 4: Complete Migration
┌────────────────────────────────────────┐
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │  Users   │ │  Orders  │ │ Payments │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│                                        │
│ ┌──────────┐ ┌──────────┐              │
│ │Inventory │ │Notif.    │              │
│ └──────────┘ └──────────┘              │
└────────────────────────────────────────┘

Project: Service Decomposition Exercise

Decompose this e-commerce monolith into microservices:

Current Monolith Features:

  • User registration and authentication
  • Product catalog management
  • Shopping cart
  • Order placement and tracking
  • Payment processing
  • Inventory management
  • Reviews and ratings
  • Notifications (email, SMS, push)
  • Search functionality
  • Recommendations

Your Task:

  1. Identify Services: List the microservices you would create
  2. Define Boundaries: What does each service own?
  3. Data Ownership: Which database for each service?
  4. Communication: How do services communicate?
  5. Dependencies: Draw the dependency graph
Proposed Services:
ServiceResponsibilityDatabaseCommunication
User ServiceAuth, profiles, preferencesPostgreSQLREST API
Product ServiceCatalog, categoriesPostgreSQL + ElasticsearchREST API
Cart ServiceShopping cart managementRedisREST API
Order ServiceOrder lifecyclePostgreSQLEvents + REST
Payment ServicePayment processingPostgreSQLEvents + REST
Inventory ServiceStock managementRedis + PostgreSQLEvents
Review ServiceReviews and ratingsMongoDBREST API
Notification ServiceAll notificationsPostgreSQLEvents only
Search ServiceProduct searchElasticsearchREST API
Recommendation ServiceML recommendationsRedisREST API
Dependency Graph:
            ┌─────────────────┐
            │   API Gateway   │
            └────────┬────────┘

     ┌───────────────┼───────────────┐
     │               │               │
     ▼               ▼               ▼
┌─────────┐    ┌─────────┐    ┌─────────┐
│  User   │    │ Product │    │  Cart   │
│ Service │    │ Service │    │ Service │
└─────────┘    └────┬────┘    └────┬────┘
                    │              │
              ┌─────┴─────┐        │
              ▼           ▼        ▼
        ┌─────────┐  ┌─────────────────┐
        │ Search  │  │  Order Service  │
        │ Service │  └────────┬────────┘
        └─────────┘           │
                    ┌─────────┴─────────┐
                    ▼                   ▼
              ┌─────────┐        ┌─────────┐
              │ Payment │        │Inventory│
              │ Service │        │ Service │
              └────┬────┘        └────┬────┘
                   │                  │
                   └────────┬─────────┘

                    ┌─────────────────┐
                    │  Notification   │
                    │    Service      │
                    └─────────────────┘

Interview Questions

Answer:
  • Small team (< 10 developers)
  • Startup MVP still validating product-market fit
  • Simple CRUD applications
  • Limited DevOps capabilities
  • Unclear domain boundaries
  • Strong consistency requirements across domains
  • When network latency would significantly impact user experience
Answer: Use Domain-Driven Design (DDD) concepts:
  1. Bounded Contexts: Identify areas where models/language differ
  2. Business Capabilities: Align with business functions
  3. Data Ownership: Who owns the data?
  4. Team Structure: Conway’s Law considerations
  5. Change Frequency: What changes together?
Avoid:
  • Splitting by technical layers (UI, DB, API)
  • Creating nano-services
  • Sharing databases between services
Answer: A migration strategy to gradually replace a monolith:
  1. Identify a bounded context to extract
  2. Build the new microservice alongside the monolith
  3. Route traffic to new service (using API gateway/proxy)
  4. Migrate data
  5. Decommission old code
  6. Repeat for next context
Benefits: Low risk, gradual migration, always have working system
Answer: A poorly designed microservices system that has:
  • Tight coupling between services
  • Shared databases
  • Synchronous call chains
  • Must deploy multiple services together
  • Single point of failure affects everything
It has all the complexity of microservices with none of the benefits.

Summary

Key Takeaways

  • Microservices are not always the answer
  • Start with a monolith, extract when needed
  • Define clear service boundaries
  • Each service owns its data
  • Design for failure from the start

Next Steps

In the next chapter, we’ll dive into Domain-Driven Design and learn how to properly identify and define service boundaries using bounded contexts.