Skip to content
Bitloops - Git captures what changed. Bitloops captures why.
HomeAbout usDocsBlog
ResourcesSoftware ArchitectureMicroservices Architecture: Breaking the Monolith

Microservices Architecture: Breaking the Monolith

Microservices let teams and services scale independently; each owns data and deploys alone. But operational complexity is real. Worth it for large teams and complex domains. Poor boundaries create distributed monoliths with worse problems than monoliths.

9 min readUpdated March 4, 2026Software Architecture

Definition

Microservices are independently deployable services, each owning its data and communicating via well-defined interfaces. They're organized around business capabilities, not technical layers.

A typical e-commerce system might have: Users Service, Products Service, Orders Service, Payments Service, Shipping Service. Each service is a complete vertical slice—it has its database, business logic, and API. Teams own services end-to-end.

The core promise: change one service without affecting others. Deploy one service without deploying everything. Scale one service independently. Organize teams by service ownership.

Why Microservices Exist

Organizations face scaling problems. A monolithic system with one team works fine. Add five teams, and they're stepping on each other. Different teams want different databases, release schedules, technology choices. A monolith forces consensus.

Microservices solve this by inverting the problem: instead of one system with many teams, many systems with one team each. Each team owns a service. Each service can be changed, deployed, scaled independently.

This is Conway's Law applied intentionally: design the system to match your org structure.

Service Boundaries: The Critical Decision

Everything falls apart if you get this wrong. Services must have clear, stable boundaries. The question: what belongs in a service?

Not by technology layer. "A service for the API layer, another for business logic, another for data access." This is a distributed monolith with extra latency. Avoid it.

Not by CRUD operations. "A service for creating users, another for updating, another for deleting." Too many services, high coupling.

By business capability or bounded context. An Orders Service owns all order operations. It has creation, updates, deletion, queries—all in one service. It owns the Order entity and the database schema. No other service touches Order data directly.

The best approach: align services to bounded contexts from Domain-Driven Design. Each service is a bounded context with its own language, rules, and data.

Bounded Context: Users
  - Service: Users Service
  - Owns: User aggregate, Profile, Preferences
  - API: CreateUser, UpdateProfile, GetUser, DeleteUser
  - Database: users_db

Bounded Context: Orders
  - Service: Orders Service
  - Owns: Order aggregate, OrderItem, OrderStatus
  - API: CreateOrder, UpdateOrder, GetOrder, CancelOrder
  - Database: orders_db

Bounded context: Payments
  - Service: Payments Service
  - Owns: Payment aggregate, PaymentMethod, Transaction
  - API: ChargePayment, RefundPayment, GetTransaction
  - Database: payments_db
YAML

Services communicate through well-defined APIs. They don't share databases, tables, or internal objects. Each service is a black box to the others.

Service Communication: Sync vs. Async

Synchronous (Request-Reply)

A service makes an HTTP or RPC call and waits for a response.

// Orders Service calls Payments Service
class CreateOrderUseCase {
  constructor(
    private orderRepository: OrderRepository,
    private paymentsService: PaymentsServiceClient
  ) {}

  async execute(request: CreateOrderRequest): Promise<Order> {
    const order = new Order(request.customerId, request.items);
    order.calculateTotal();

    // Sync call: wait for payment response
    const paymentResult = await this.paymentsService.charge(
      request.customerId,
      order.total
    );

    if (!paymentResult.success) {
      throw new PaymentFailedError('Charge failed');
    }

    await this.orderRepository.save(order);
    return order;
  }
}
javascript

Cons: If Payments Service is slow or down, Orders Service is blocked. Creates temporal coupling. Hard to scale independently.

Asynchronous (Event-Based)

A service publishes an event. Other services react to it. No direct calls, no blocking.

// Orders Service publishes an event
class CreateOrderUseCase {
  constructor(
    private orderRepository: OrderRepository,
    private eventBus: EventBus
  ) {}

  async execute(request: CreateOrderRequest): Promise<Order> {
    const order = new Order(request.customerId, request.items);
    order.calculateTotal();

    await this.orderRepository.save(order);

    // Async publish: don't wait for response
    await this.eventBus.publish(
      new OrderCreatedEvent(order.id, order.customerId, order.total)
    );

    return order;
  }
}

// Payments Service listens to the event
class PaymentEventListener {
  async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
    const payment = new Payment(event.orderId, event.total);
    const result = await this.chargePayment(payment);

    if (result.success) {
      await this.eventBus.publish(new PaymentProcessedEvent(event.orderId));
    } else {
      await this.eventBus.publish(new PaymentFailedEvent(event.orderId));
    }
  }
}
typescript

Pros: Services are decoupled. If Payments Service is down, Orders Service continues. Easy to scale independently. Resilient.

Cons: Eventual consistency (Order is created before payment is confirmed). Harder to debug (causality is invisible). Requires careful error handling (what if payment fails after order is created?).

Most microservices systems use both. Happy path queries are sync (get a user quickly). Side effects are async (order created, send email, update analytics).

Data Ownership: The Core Principle

This is non-negotiable: each service owns its database. Other services don't query it directly. They request data via the service's API.

// WRONG: Orders Service queries Users database
const user = await usersDatabase.query(
  'SELECT * FROM users WHERE id = $1',
  [customerId]
);

// RIGHT: Orders Service calls Users Service API
const user = await usersService.getUser(customerId);
javascript

Why? Because the Users database schema is an implementation detail of the Users Service. When the Users team refactors the schema, they don't break other services.

If Orders Service queries the Users database directly, it couples to that schema. Now the Users team can't change it without coordinating with the Orders team. Data ownership dissolves.

The Microservices Contract

Services communicate through contracts:

// Users Service: API contract
interface IUsersService {
  getUser(id: string): Promise<UserDTO>;
  createUser(data: CreateUserRequest): Promise<UserDTO>;
  updateUser(id: string, data: UpdateUserRequest): Promise<void>;
}

// Orders Service: API contract
interface IOrdersService {
  createOrder(data: CreateOrderRequest): Promise<OrderDTO>;
  getOrder(id: string): Promise<OrderDTO>;
  cancelOrder(id: string): Promise<void>;
}

// Payments Service: API contract
interface IPaymentsService {
  charge(customerId: string, amount: number): Promise<PaymentResult>;
  refund(paymentId: string): Promise<void>;
}
typescript

These are contracts. Services are responsible for not breaking them. If Users Service adds a field to UserDTO, it doesn't break Orders Service (it just ignores the new field). If it removes a field, it breaks the contract and requires coordination.

Contracts can be versioned: /v1/users/ and /v2/users/. Old clients call v1, new clients call v2. The service supports both for a transition period.

Common Pitfalls

Distributed monolith. You've split the codebase but not the architecture. Services are tightly coupled via sync calls. One service is down, everything fails. You have the complexity of microservices with none of the benefits. This is the most common failure mode.

Chatty services. Orders Service needs to call Users Service, which calls Billing Service, which calls Analytics Service. One request becomes four service calls. Latency explodes. User waits 2 seconds. This is the microservices tax.

Shared database. Services share a database to "reduce complexity." But then they're not really separate. They can't scale independently. They can't use different technologies. This defeats the purpose.

Eventual consistency without planning. You publish an event. Services react. But what if the reaction fails? What if it's too slow? Eventual consistency requires careful design: idempotency keys, deduplication, retry logic, timeouts, circuit breakers.

Too many services. "Each endpoint is a service." You have 50 services communicating with each other. Complexity explodes. Debugging is impossible. The operational overhead is crushing.

When Microservices Make Sense

Large, distributed teams. If you have five independent teams, five services is sensible. Each team owns a service.

Independent scaling needs. The Orders Service needs to scale 10x but Users Service is stable. Microservices let you scale them independently.

Different technology stacks. One team wants Java, another wants Node.js, another wants Python. Microservices allow it.

Complex domain with clear subdomain boundaries. E-commerce has clear subdomains: users, products, orders, payments, shipping. Each subdomain is a service.

Frequent independent deployments. You deploy Payments Service several times a day. Users Service monthly. Monoliths force coordination. Microservices don't.

When Microservices Are Overkill

Small, focused teams. One or two developers, focused on a single feature set. A monolith is simpler.

Simple CRUD operations. Most logic is validation and persistence. Microservices' complexity is overhead.

Tightly interdependent logic. Everything depends on everything. Services are too coupled for independence. A monolith is simpler.

Prototypes and MVPs. Building to learn. The overhead of setting up service infrastructure, messaging, monitoring slows you down. Build a monolith first.

Low deployment frequency. If you deploy quarterly, monoliths are fine. The overhead of orchestrating microservices deployments isn't justified.

The Operational Tax

Microservices aren't free. You get:

  • Distributed tracing and monitoring. Is the problem in Payments Service or Orders Service? You need observability across services.
  • Service discovery. Where does Users Service live? Is it localhost:3000 or service.cluster.local?
  • Circuit breakers and retry logic. Network calls fail. You need strategies to handle failure gracefully.
  • Database migration coordination. If Orders Service's schema changes and needs to communicate with Payments Service, you need careful sequencing.
  • Testing complexity. Integration tests now involve multiple services. Setup is harder.
  • Operational overhead. More moving parts to deploy, monitor, scale.

This is the "microservices premium." You pay it in complexity, tooling, and ops effort. You get it back in scalability and team independence.

For small teams, the premium is worse than the benefit. For large teams with complex domains, the premium is worth it.

From Monolith to Microservices

Don't start with microservices. Start with a monolith. As it grows and pain points emerge, split strategically:

  1. Identify hot spots. Where is coordination overhead highest? Where do teams block each other?
  2. Extract a service. Take the first painful feature set and extract it. Usually this is Users or Payments—something with clear boundaries.
  3. Define the contract. How will the new service communicate with the monolith? HTTP API? RPC? Events?
  4. Migrate gradually. Move some calls to the service, others stay in the monolith. Both work together.
  5. Iterate. Once the first service works, extract the next. This takes months or years.

Most successful migrations take 18-24 months and extract 3-5 services before the full benefit emerges.

AI Agents and Microservices

Microservices architectures need clear boundaries to stay intact with AI-assisted development. An agent might write code that violates service boundaries (calling another service's database, creating implicit dependencies).

Bitloops helps here. You define service contracts: "Orders Service can call Users Service API via this interface." "Orders Service cannot call the users_db directly." An agent generating code gets immediate feedback: this call violates service boundaries.

Moreover, API contracts become part of the context you provide to agents. "Here's the Users Service API you can call. Here's the schema you get back." The agent has clear, explicit guidance on how to interact with other services.

FAQ

How many services is too many?

There's no fixed number, but more than 20 is concerning. Each service adds operational overhead. If you have 50 services, 40% of your effort goes to infrastructure, 60% to features. Not a good tradeoff.

Should each service have its own database?

Yes. If they share a database, they're not really separate services. They can't scale independently, can't use different technologies, can't evolve independently.

How do I handle transactions across services?

Carefully. Distributed transactions (two-phase commit) are slow and brittle. Better approaches: sagas (a sequence of transactions with compensating actions if one fails) or accepting eventual consistency (process the order, eventually process payment).

Can I use a monolith and microservices together?

Yes. Common pattern: a monolithic core with specialized services around it. The monolith handles the majority of logic, services handle specific high-volume or high-complexity concerns.

How do I test microservices?

Unit tests for each service (fast, no integration). Contract tests to verify service boundaries (verify Payments Service expects what Orders Service sends). Integration tests with real services (slow, comprehensive). Keep most tests unit-level, fewer contract tests, few integration tests.

What about versioning service APIs?

Version when you break contracts. Support old versions for 3-6 months so clients can migrate. Ideally, design APIs to be backward compatible (adding fields is safe, removing fields breaks contracts).

Primary Sources

Get Started with Bitloops.

Apply what you learn in these hubs to real AI-assisted delivery workflows with shared context, traceable reasoning, and architecture-aware engineering practices.

curl -sSL https://bitloops.com/install.sh | bash