#Microservices#DevOps#Backend#System Design#Architecture

Microservices Architecture and Modern Application Development Strategies

Transitioning from monolithic applications to microservices architecture: Advantages, challenges, and best practices in application development

Enes Karakuş

Enes Karakuş

Backend Developer & System Architect

7/3/2025
11 dk okuma

Microservices Architecture and Modern Application Development Strategies#

The software world is undergoing a transformation from large monolithic applications to smaller, independent, and specialized groups of services. Microservices architecture is a modern application development approach that has gained popularity in recent years and has been adopted by many large technology companies. In this article, we'll explore the fundamentals of microservices architecture, its advantages, challenges, and best practices for a successful microservices strategy.

Monolithic Architecture vs Microservices Architecture#

What is Monolithic Architecture?#

Monolithic architecture is the traditional approach where all application components run in a single codebase as a single process.

Advantages:

  • Simpler at the beginning of development
  • Easier deployment
  • Simpler end-to-end testing
  • Ease of working with a single codebase

Disadvantages:

  • Scalability issues
  • Increasing complexity as the codebase grows
  • Difficulty in making technology changes
  • Long build and deployment times
  • Limited team independence

What is Microservices Architecture?#

Microservices architecture is an approach that structures an application as small, independent services focused on specific functionality. Each service:

  • Runs in its own process
  • Communicates through simple, well-defined APIs
  • Can be developed, deployed, and scaled independently
  • Usually focuses on a single business function

Advantages:

  • Improved scalability
  • Technology diversity (ability to choose the most suitable technology for each service)
  • Faster deployments and updates
  • Better code organization
  • Increased fault isolation
  • Parallel work by independent teams

Disadvantages:

  • Increased operational complexity
  • Challenges of distributed systems
  • Management of inter-service communication
  • More difficult end-to-end testing
  • Challenges in consistent monitoring and debugging

Core Principles of Microservices Architecture#

1. Single Responsibility Principle#

Each microservice should focus on a specific business domain or functionality and only take on responsibilities related to that area.

// User Service - Only concerned with user management
class UserService {
  async createUser(userData) {
    // User creation process
  }
  
  async getUserById(id) {
    // Get user by ID
  }
  
  async updateUser(id, userData) {
    // Update user
  }
}

// Payment Service - Only concerned with payment operations
class PaymentService {
  async processPayment(paymentDetails) {
    // Process payment
  }
  
  async refundPayment(paymentId) {
    // Payment refund
  }
}

2. Independent Deployment#

Each microservice should be independently deployable. This enables easier implementation of continuous integration and continuous deployment (CI/CD) processes.

# GitHub Actions CI/CD configuration for User Service
name: User Service CI/CD

on:
  push:
    branches: [ main ]
    paths:
      - 'services/user-service/**'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: cd services/user-service && npm install
        
      - name: Run tests
        run: cd services/user-service && npm test
        
      - name: Build and push Docker image
        uses: docker/build-push-action@v2
        with:
          context: ./services/user-service
          push: true
          tags: myregistry.io/user-service:latest
          
      - name: Deploy to Kubernetes
        uses: steebchen/kubectl@v2
        with:
          config: ${{ secrets.KUBE_CONFIG }}
          command: apply -f services/user-service/k8s/deployment.yaml

3. Data Management Independence#

Each microservice should manage its own database and should not directly access the databases of other services. This reduces dependencies between services and ensures that database schema changes remain isolated.

// User Service - Uses its own database
const userDb = require('./user-database');

class UserRepository {
  async findById(id) {
    return userDb.collection('users').findOne({ _id: id });
  }
}

// Order Service - Uses its own database
const orderDb = require('./order-database');

class OrderRepository {
  async findByUserId(userId) {
    return orderDb.collection('orders').find({ userId }).toArray();
  }
}

4. Smart Endpoints, Simple Pipes#

Inter-service communication should occur through simple protocols (typically HTTP/REST, gRPC, or message queues). Complex communication mechanisms increase the complexity of the system.

// Order Service - Getting data from User Service over HTTP
const axios = require('axios');

class OrderService {
  constructor(userServiceUrl) {
    this.userServiceUrl = userServiceUrl;
  }
  
  async createOrder(userId, products) {
    // Validate user information
    try {
      const userResponse = await axios.get(`${this.userServiceUrl}/users/${userId}`);
      const user = userResponse.data;
      
      if (!user.isActive) {
        throw new Error('Inactive user cannot place orders');
      }
      
      // Order creation processes
      // ...
    } catch (error) {
      // Error handling
    }
  }
}

5. Independent Scalability#

Each service should be independently scalable according to workload requirements.

# Kubernetes Horizontal Pod Autoscaler example
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payment-service-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Microservices Architecture Design Patterns#

1. API Gateway Pattern#

API Gateway acts as an intermediary layer between clients and microservices. It performs functions such as request routing, aggregation, authentication, and rate limiting.

// A simple API Gateway implementation with Node.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const jwt = require('express-jwt');

const app = express();

// Authentication middleware
const authMiddleware = jwt({
  secret: process.env.JWT_SECRET,
  algorithms: ['HS256']
});

// Rate limiting middleware
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // maximum requests per IP
});

// Service proxy definitions
app.use('/api/users', authMiddleware, limiter, createProxyMiddleware({ 
  target: 'http://user-service:8080',
  pathRewrite: {'^/api/users': '/'}
}));

app.use('/api/orders', authMiddleware, limiter, createProxyMiddleware({ 
  target: 'http://order-service:8080',
  pathRewrite: {'^/api/orders': '/'}
}));

app.use('/api/payments', authMiddleware, limiter, createProxyMiddleware({ 
  target: 'http://payment-service:8080',
  pathRewrite: {'^/api/payments': '/'}
}));

// API aggregation endpoint
app.get('/api/user-dashboard/:userId', authMiddleware, async (req, res) => {
  try {
    const userId = req.params.userId;
    
    // Parallel requests
    const [userResponse, ordersResponse, paymentsResponse] = await Promise.all([
      axios.get(`http://user-service:8080/users/${userId}`),
      axios.get(`http://order-service:8080/orders/user/${userId}`),
      axios.get(`http://payment-service:8080/payments/user/${userId}`)
    ]);
    
    // Combining responses
    const dashboardData = {
      user: userResponse.data,
      recentOrders: ordersResponse.data.slice(0, 5),
      paymentMethods: paymentsResponse.data
    };
    
    res.json(dashboardData);
  } catch (error) {
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.listen(8000, () => {
  console.log('API Gateway running on port 8000');
});

2. Aggregator Pattern#

A pattern that collects data from multiple microservices, combines them, and presents them as a single response to the client.

// Aggregator for product detail page
class ProductDetailAggregator {
  constructor(
    productService,
    inventoryService,
    reviewService,
    recommendationService
  ) {
    this.productService = productService;
    this.inventoryService = inventoryService;
    this.reviewService = reviewService;
    this.recommendationService = recommendationService;
  }
  
  async getProductDetails(productId) {
    try {
      // Parallel requests
      const [product, inventory, reviews, recommendations] = await Promise.all([
        this.productService.getProduct(productId),
        this.inventoryService.getInventory(productId),
        this.reviewService.getReviews(productId, { limit: 5 }),
        this.recommendationService.getRecommendations(productId)
      ]);
      
      // Combining data
      return {
        id: product.id,
        name: product.name,
        description: product.description,
        price: product.price,
        images: product.images,
        availability: inventory.inStock ? 'In Stock' : 'Out of Stock',
        quantity: inventory.quantity,
        estimatedDelivery: inventory.estimatedDelivery,
        averageRating: reviews.averageRating,
        reviewCount: reviews.total,
        topReviews: reviews.items,
        recommendations: recommendations.slice(0, 4)
      };
    } catch (error) {
      throw new Error(`Failed to aggregate product details: ${error.message}`);
    }
  }
}

3. Circuit Breaker Pattern#

A pattern that prevents requests to faulty services, preventing cascade failures and protecting the overall health of the system.

// Circuit breaker example with Opossum for Node.js
const CircuitBreaker = require('opossum');
const axios = require('axios');

class PaymentGatewayService {
  constructor(gatewayUrl) {
    this.gatewayUrl = gatewayUrl;
    
    // Circuit breaker configuration
    const options = {
      timeout: 3000, // 3 seconds
      errorThresholdPercentage: 50, // Opens at 50% error rate
      resetTimeout: 30000, // Tries again after 30 seconds
      rollingCountTimeout: 60000, // 1 minute window
      rollingCountBuckets: 10, // 10 buckets
    };
    
    this.breaker = new CircuitBreaker(this.processPayment.bind(this), options);
    
    // Listen to circuit breaker events
    this.breaker.on('open', () => console.log('Circuit Breaker opened - payment service is down'));
    this.breaker.on('halfOpen', () => console.log('Circuit Breaker half-open - trying to recover'));
    this.breaker.on('close', () => console.log('Circuit Breaker closed - payment service recovered'));
    this.breaker.on('fallback', () => console.log('Circuit Breaker fallback executed'));
    
    // Define fallback operation
    this.breaker.fallback(() => {
      return {
        success: false,
        message: 'Payment service unavailable, please try again later',
        fallback: true
      };
    });
  }
  
  async processPayment(paymentData) {
    const response = await axios.post(`${this.gatewayUrl}/process`, paymentData);
    return response.data;
  }
  
  async makePayment(paymentData) {
    return this.breaker.fire(paymentData);
  }
}

4. Command Query Responsibility Segregation (CQRS)#

A pattern that separates command (data writing) and query (data reading) responsibilities into separate models.

// CQRS implementation - Command side
class OrderCommandService {
  constructor(eventBus, orderRepository) {
    this.eventBus = eventBus;
    this.orderRepository = orderRepository;
  }
  
  async createOrder(orderData) {
    // Business logic and validation
    const order = {
      id: uuid(),
      userId: orderData.userId,
      products: orderData.products,
      totalAmount: this.calculateTotal(orderData.products),
      status: 'CREATED',
      createdAt: new Date()
    };
    
    // Write to database
    await this.orderRepository.save(order);
    
    // Publish event
    await this.eventBus.publish('OrderCreated', order);
    
    return { orderId: order.id };
  }
  
  async updateOrderStatus(orderId, status) {
    const order = await this.orderRepository.findById(orderId);
    if (!order) throw new Error('Order not found');
    
    order.status = status;
    order.updatedAt = new Date();
    
    await this.orderRepository.save(order);
    await this.eventBus.publish('OrderStatusUpdated', { orderId, status });
    
    return { success: true };
  }
  
  calculateTotal(products) {
    // Calculate total price
  }
}

// CQRS implementation - Query side (can use separate database)
class OrderQueryService {
  constructor(orderReadModel) {
    this.orderReadModel = orderReadModel;
  }
  
  async getOrderById(orderId) {
    return this.orderReadModel.findOne({ id: orderId });
  }
  
  async getUserOrders(userId, options) {
    const { page = 1, limit = 10, status } = options;
    const query = { userId };
    
    if (status) {
      query.status = status;
    }
    
    return this.orderReadModel.find(query)
      .sort({ createdAt: -1 })
      .skip((page - 1) * limit)
      .limit(limit);
  }
  
  async getOrderStatistics(filters) {
    // Run statistic queries
    return this.orderReadModel.aggregate([
      { $match: filters },
      { $group: { _id: "$status", count: { $sum: 1 } } }
    ]);
  }
}

5. Saga Pattern#

A pattern used to ensure data consistency in distributed transactions.

// Saga pattern implementation for order creation
class OrderSaga {
  constructor(
    orderService,
    paymentService,
    inventoryService,
    notificationService
  ) {
    this.orderService = orderService;
    this.paymentService = paymentService;
    this.inventoryService = inventoryService;
    this.notificationService = notificationService;
  }
  
  async execute(orderData) {
    let orderId;
    
    try {
      // Step 1: Create Order
      const orderResult = await this.orderService.createOrder(orderData);
      orderId = orderResult.orderId;
      
      // Step 2: Reserve Inventory
      await this.inventoryService.reserveItems(orderId, orderData.items);
      
      // Step 3: Process Payment
      await this.paymentService.processPayment({
        orderId,
        amount: orderData.totalAmount,
        paymentDetails: orderData.paymentDetails
      });
      
      // Step 4: Confirm Order
      await this.orderService.confirmOrder(orderId);
      
      // Step 5: Notify Customer
      await this.notificationService.sendOrderConfirmation(orderId);
      
      return { success: true, orderId };
      
    } catch (error) {
      // Compensating transactions on failure
      console.error(`Order saga failed: ${error.message}`);
      
      if (orderId) {
        if (error.step === 'payment') {
          // Rollback inventory reservation
          await this.inventoryService.releaseItems(orderId, orderData.items);
        }
        
        if (error.step === 'inventory' || error.step === 'payment') {
          // Cancel order
          await this.orderService.cancelOrder(orderId, `Failed at ${error.step} step`);
        }
        
        // Notify customer about failure
        await this.notificationService.sendOrderFailure(orderId, error.message);
      }
      
      throw new Error(`Order creation failed: ${error.message}`);
    }
  }
}

Domain-Driven Design in Microservices#

Domain-Driven Design (DDD) provides an excellent foundation for defining service boundaries in a microservices architecture.

// Order Aggregate in DDD
class Order {
  constructor(id, customerId, items) {
    this.id = id;
    this.customerId = customerId;
    this.items = items;
    this.status = 'PENDING';
    this.totalAmount = this.calculateTotal(items);
    this.createdAt = new Date();
    
    this.events = []; // Domain events
    this.events.push({
      type: 'ORDER_CREATED',
      orderId: this.id,
      customerId: this.customerId,
      items: this.items,
      occurredAt: new Date()
    });
  }
  
  calculateTotal(items) {
    return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }
  
  approve() {
    if (this.status !== 'PENDING') {
      throw new Error(`Cannot approve order in ${this.status} status`);
    }
    
    this.status = 'APPROVED';
    this.approvedAt = new Date();
    
    this.events.push({
      type: 'ORDER_APPROVED',
      orderId: this.id,
      occurredAt: new Date()
    });
  }
  
  ship() {
    if (this.status !== 'APPROVED') {
      throw new Error(`Cannot ship order in ${this.status} status`);
    }
    
    this.status = 'SHIPPED';
    this.shippedAt = new Date();
    
    this.events.push({
      type: 'ORDER_SHIPPED',
      orderId: this.id,
      occurredAt: new Date()
    });
  }
  
  cancel(reason) {
    if (!['PENDING', 'APPROVED'].includes(this.status)) {
      throw new Error(`Cannot cancel order in ${this.status} status`);
    }
    
    this.status = 'CANCELLED';
    this.cancellationReason = reason;
    this.cancelledAt = new Date();
    
    this.events.push({
      type: 'ORDER_CANCELLED',
      orderId: this.id,
      reason,
      occurredAt: new Date()
    });
  }
  
  deliver() {
    if (this.status !== 'SHIPPED') {
      throw new Error(`Cannot deliver order in ${this.status} status`);
    }
    
    this.status = 'DELIVERED';
    this.deliveredAt = new Date();
    
    this.events.push({
      type: 'ORDER_DELIVERED',
      orderId: this.id,
      occurredAt: new Date()
    });
  }
}

// Domain Service
class OrderService {
  constructor(orderRepository, paymentService, inventoryService) {
    this.orderRepository = orderRepository;
    this.paymentService = paymentService;
    this.inventoryService = inventoryService;
  }
  
  async createOrder(orderData) {
    // Apply business rules
    const order = new Order(
      uuid(),
      orderData.customerId,
      orderData.items
    );
    
    // Save the aggregate
    await this.orderRepository.save(order);
    
    // Process domain events
    for (const event of order.events) {
      await this.publishEvent(event);
    }
    
    return order;
  }
  
  async approveOrder(orderId) {
    const order = await this.orderRepository.findById(orderId);
    if (!order) {
      throw new Error('Order not found');
    }
    
    // Business rule check - inventory check
    const inventoryCheck = await this.inventoryService.checkAvailability(order.items);
    if (!inventoryCheck.available) {
      throw new Error('Insufficient inventory');
    }
    
    // Business rule check - payment check
    const paymentResult = await this.paymentService.processPayment({
      orderId: order.id,
      customerId: order.customerId,
      amount: order.totalAmount
    });
    
    if (!paymentResult.success) {
      throw new Error(`Payment failed: ${paymentResult.message}`);
    }
    
    // Update the aggregate
    order.approve();
    await this.orderRepository.save(order);
    
    // Process domain events
    for (const event of order.events) {
      await this.publishEvent(event);
    }
    
    return order;
  }
  
  async publishEvent(event) {
    // Event publishing infrastructure (e.g., Kafka)
    console.log(`Publishing event: ${event.type}`, event);
  }
}

Conclusion#

Microservices architecture is a powerful approach in modern application development, but it may not be suitable for every project. Before adopting this architecture, it's important to carefully evaluate your team's capabilities, the complexity of existing systems, and your actual business needs.

A successful microservices strategy requires:

  1. Clearly defined service boundaries
  2. Strong DevOps infrastructure and culture
  3. Automated tests and CI/CD pipelines
  4. Monitoring and logging strategy
  5. Standardization of inter-service communication
  6. Resilience and fault tolerance patterns

By applying these principles and patterns, you can develop and operate scalable, flexible, and sustainable modern applications.

If you liked this article, you can share it on social media.
Enes Karakuş

Enes Karakuş

Microservice architecture and API systems expert. Develops cloud solutions with Kubernetes and Docker. Experienced in database and system architecture.

Get Support for Your Project

Get professional development services for modern web applications.

Contact Us