Extracting services from a monolith is less about carving out pieces and more about establishing new, independent communication channels while slowly starving the monolithic code of its responsibilities.

Let’s say you have a monolithic e-commerce application. It handles everything: user authentication, product catalog, order processing, inventory management, and payment gateway integration. Your goal is to extract the "Order Processing" service.

Here’s how that might look in practice, without a complete rewrite:

// Initial Monolith (Conceptual)
{
  "services": {
    "UserAuth": { ... },
    "ProductCatalog": { ... },
    "OrderProcessing": {
      "createOrder": (userId, items) => { /* ... database writes, inventory checks ... */ },
      "getOrderDetails": (orderId) => { /* ... database reads ... */ }
    },
    "InventoryManagement": { ... },
    "PaymentGateway": { ... }
  }
}

// Target State: Order Service as a separate microservice
// The monolith now CALLS the new Order Service API.

// Order Service (New Microservice)
{
  "api": {
    "POST /orders": (requestBody) => {
      // 1. Validate request
      // 2. Interact with InventoryService (new call)
      // 3. Interact with PaymentGateway (new call)
      // 4. Persist order to its own database
      // 5. Return order confirmation
    },
    "GET /orders/{orderId}": (orderId) => {
      // 1. Read order from its own database
    }
  }
}

The core problem this solves is the entropic decay of large codebases. As a monolith grows, dependencies become tangled, deployments become risky, and innovation slows because every change risks breaking something unrelated. Extracting services allows teams to own smaller, more manageable pieces of the system, leading to faster development cycles, independent deployments, and the ability to choose the best technology for a specific job.

Internally, this migration is often achieved through a series of steps, not a single "big bang" event. You’ll typically start by identifying a bounded context – a distinct area of functionality with its own domain model and logic. For "Order Processing," this means all the code that deals with creating, updating, and querying orders.

The first practical step is often to duplicate the relevant data. The Order Processing service will need its own database. Initially, this might be a read replica of the monolith’s order table, or a separate schema pointing to the same underlying database instance. Over time, this data will be fully migrated.

The next phase is to introduce an anti-corruption layer. This is a set of adapters or facades within the monolith that translate requests from the old monolithic style into the new API calls for the extracted service.

// Inside the Monolith (before extraction)
public Order createOrder(UserId userId, List<Item> items) {
    // ... direct database access, inventory checks, payment calls ...
}

// Inside the Monolith (after extraction, using an OrderServiceClient)
public Order createOrder(UserId userId, List<Item> items) {
    // 1. Call the new OrderService API (e.g., via REST or gRPC)
    OrderService.CreateOrderRequest request = new OrderService.CreateOrderRequest(userId, items);
    OrderResponse response = orderServiceClient.createOrder(request);

    // 2. Translate the response back into the monolith's internal Order object
    return mapResponseToOrder(response);
}

You’ll also need to handle inter-service communication. If the monolith needs to call the new Order Service, you’ll implement a client library or use a service mesh. If the new Order Service needs to call other services (like Inventory or Payment), it will use its own client libraries, and these will be entirely separate from the monolith’s internal calls. This is where you start to see the true decoupling.

The key levers you control are:

  • Bounded Context Identification: Which piece of functionality is truly independent? This is more art than science, guided by domain-driven design principles.
  • Data Ownership: When does the new service own its data exclusively? This is a phased approach, moving from shared to replicated to independent.
  • Communication Patterns: How will services talk to each other? Synchronous (REST, gRPC) or asynchronous (message queues like Kafka or RabbitMQ)? The choice impacts performance, resilience, and complexity.
  • Deployment Strategy: How will you roll out the new service and migrate traffic? Canary releases, blue-green deployments, or feature flags are common.

The most surprising thing is how much of the "monolith" code can remain unchanged initially. The extraction is often about building the new system around the old, and then gradually redirecting traffic and functionality, rather than a full rewrite. You’re essentially creating a parallel universe for the extracted service and then switching the stargates.

Once you’ve successfully extracted the Order Processing service, the next logical step is to tackle the Inventory Management service, which likely has its own set of data and interactions with the order lifecycle.

Want structured learning?

Take the full Microservices course →