Skip to main content

Sign in to CleanKata

Track your progress, earn XP, and unlock every lesson.

By signing in you agree to our Terms of Use and Privacy Policy.

Clean Architecture70 XP7 min

Decoupling Modes: Source, Deployment, and Service

Decoupling can happen at source code, deployment, or service level — good architecture lets you start as a monolith and evolve into services if boundaries are well-defined.

Why this matters

There are three modes of decoupling, each with different costs and benefits. Source-level decoupling means shared source code in a single deployable — teams can work in separate modules but still step on each other at deploy time. Deployment-level decoupling means separate compiled artifacts (packages, JARs, DLLs) that can be independently deployed — faster to release one component without rebuilding others. Service-level decoupling means separate processes communicating over a network — the strongest isolation, but also the highest cost (latency, network failures, data serialization).

The key insight is that you should not jump straight to service-level decoupling. A monolith with well-drawn boundaries — using abstract ports and adapters — is architecturally ready to be split into services when the business need arises. A poorly drawn distributed system is the worst outcome: you pay the network cost but get none of the isolation benefits.

✗The problem

Orders and inventory tightly coupled via direct imports and a shared database session — impossible to split without major surgery, impossible to scale independently.

Bad

# orders/service.py
from inventory.manager import InventoryManager  # direct coupling

class OrderService:
    def __init__(self, db):
        self.db        = db
        self.inventory = InventoryManager(db)   # shared DB session!

    def place(self, user_id, item_id, qty):
        if not self.inventory.check_and_decrement(item_id, qty):
            raise ValueError("Out of stock")
        return self.db.insert("orders", {"user_id": user_id, "item_id": item_id})

# inventory/manager.py
class InventoryManager:
    def __init__(self, db): self.db = db
    def check_and_decrement(self, item_id, qty):
        item = self.db.find("inventory", item_id)
        if item["stock"] < qty: return False
        self.db.update("inventory", item_id, {"stock": item["stock"] - qty})
        return True

# Shared DB session: inventory can never move to its own database.
# Direct import: extracting to a microservice requires rewriting both classes.
// orders/OrderService.ts
import { InventoryManager } from "../inventory/InventoryManager"; // direct coupling
import { SharedDatabase } from "../shared/Database";

export class OrderService {
  private inventory: InventoryManager;
  constructor(private db: SharedDatabase) {
    this.inventory = new InventoryManager(db);  // hard-wired, shared DB
  }

  async place(userId: string, itemId: string, qty: number): Promise {
    const ok = await this.inventory.checkAndDecrement(itemId, qty);
    if (!ok) throw new Error("Out of stock");
    return this.db.insert("orders", { userId, itemId, qty });
  }
}
// To split into microservices: rewrite both classes, handle distributed transactions.
// The boundary was never drawn — now there's nothing to promote to a service.

✓The solution

An abstract port defines the boundary — today a function call (source-level), tomorrow a REST call (service-level). OrderService never changes.

Good

from abc import ABC, abstractmethod

class InventoryPort(ABC):       # defined on the orders side
    @abstractmethod
    def reserve(self, item_id: str, qty: int) -> bool: ...

class OrderService:
    def __init__(self, order_repo, inventory: InventoryPort):
        self._orders    = order_repo
        self._inventory = inventory

    def place(self, user_id: str, item_id: str, qty: int) -> str:
        if not self._inventory.reserve(item_id, qty):
            raise ValueError("Out of stock")
        return self._orders.create(user_id, item_id, qty)

# Today — in-process adapter (source-level decoupling)
class InProcessInventoryAdapter(InventoryPort):
    def __init__(self, svc): self._svc = svc
    def reserve(self, item_id, qty): return self._svc.reserve(item_id, qty)

# Tomorrow — HTTP adapter (service-level decoupling, OrderService unchanged)
class HttpInventoryAdapter(InventoryPort):
    def __init__(self, url): self._url = url
    def reserve(self, item_id, qty):
        import httpx
        r = httpx.post(f"{self._url}/reserve", json={"item_id": item_id, "qty": qty})
        return r.json()["reserved"]
// orders/ports/InventoryPort.ts — interface owned by the orders domain
export interface InventoryPort {
  reserve(itemId: string, qty: number): Promise;
}

// orders/OrderService.ts — depends only on the port
export class OrderService {
  constructor(
    private orders: OrderRepository,
    private inventory: InventoryPort,
  ) {}

  async place(userId: string, itemId: string, qty: number): Promise {
    const ok = await this.inventory.reserve(itemId, qty);
    if (!ok) throw new Error("Out of stock");
    return this.orders.create(userId, itemId, qty);
  }
}

// Today — source-level decoupling (monolith)
export class InProcessInventoryAdapter implements InventoryPort {
  constructor(private svc: InventoryService) {}
  reserve(itemId: string, qty: number) { return this.svc.reserve(itemId, qty); }
}

// Tomorrow — service-level decoupling (microservice, OrderService unchanged)
export class HttpInventoryAdapter implements InventoryPort {
  constructor(private baseUrl: string) {}
  async reserve(itemId: string, qty: number) {
    const r = await fetch(`${this.baseUrl}/reserve`, {
      method: "POST", body: JSON.stringify({ itemId, qty }),
      headers: { "Content-Type": "application/json" },
    });
    return (await r.json()).reserved as boolean;
  }
}

💡Key takeaway

The best microservices architecture starts as a well-structured monolith. A poorly structured monolith becomes a distributed monolith — you pay the network tax but get none of the isolation benefits. Draw the boundaries first; choose the decoupling mode later.

🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.

Hint: The best microservices architecture starts as a well-structured monolith. A poorly structured monolith becomes a distributed monolith — the worst of both worlds.

✗ Your version

Decoupling Modes: Source, Deployment, and Service — CleanKata — CleanKata