Skip to main content
When a single large application contains all functionality, a failure in one place can affect the entire system. Limiting changes to smaller, isolated portions of an application reduces risk: bugs and experiments impact smaller areas, and deployments become faster and safer. Teams migrate from large monoliths to smaller, independently deployable services to gain these benefits. A monolithic application commonly shares the same codebase and process boundaries; its functions are tightly coupled and often rely on a single database for persistence—creating bottlenecks for both scale and availability.

Real-world example: the Bookinfo application

Throughout this article we use the Bookinfo example to illustrate the trade-offs. In its monolithic form, Bookinfo contains four modules bundled together:
  • details
  • reviews
  • ratings
  • product page
Although modular in concept, these modules are packaged and deployed together, share the same runtime and language, and depend on specific versions of each other. Deploying any change typically requires a full rebuild, database migration, and system release.

User flow and coupling in the monolith

  • A customer loads the product page.
  • The product page retrieves book details and reviews from the appropriate modules.
  • The reviews module queries the ratings module for rating counts.
All modules are implemented in Java and include cross-cutting concerns such as networking, authentication, authorization, data transfer logic, logging, monitoring, and tracing. Because those concerns are embedded in the monolith, an overloaded ratings module can impact the entire system. You cannot simply scale or remove the ratings module without changing code and redeploying the whole application. Adding a new module or a new team is difficult. For example, a campaign module created by a different team using another language would struggle to integrate because centralized cross-cutting functionality (like authorization) lives in the monolith. Running two versions of reviews (for A/B testing a new star UI) would require deploying the entire application twice—making experimentation costly. As systems evolve, a monolith can degrade into a “big ball of mud” where boundaries and responsibilities are unclear and technical debt accumulates.

From monolith to microservices

Refactoring a monolith into microservices is neither trivial nor instantaneous. It requires cultural, organizational, and technical change and opens the door to cloud-native patterns. In a microservices design for Bookinfo, each module becomes an independently deployable service owned by a team and implemented with technologies that match the team’s needs. For example:
  • Product page → Python
  • Details → Ruby
  • Reviews → Java (multiple versions)
  • Ratings → Node.js
Reviews can be deployed in multiple versions to support experimentation:
  • v1 (no stars)
  • v2 (black stars)
  • v3 (red stars)
The product page still calls details and reviews to render the page, but each module is deployed and scaled independently.
A diagram titled "A Monolithic Book Info App" showing interconnected service boxes for Product Page (Python), Details (Ruby), multiple Reviews versions (Java), and Ratings (Node.js). Dashed arrows indicate dependencies between the components.

Benefits of microservices

Moving to microservices delivers measurable advantages:
  • Independent scaling: scale the ratings service without affecting others.
  • Independent deployments: teams deploy individual services without coordinating full-system releases, reducing risk and cycle time.
  • Polyglot stacks: teams choose languages and frameworks that suit their domain.
  • Fault isolation: failures in one service are less likely to take down unrelated services.
  • Faster fixes and rollbacks: smaller focused releases with clear ownership improve operational agility.

Cross-cutting concerns and the problem of “fat” microservices

A naive lift-and-shift to many small services can recreate the original problem: if each service re-implements networking, authentication, authorization, service discovery, logging, monitoring, tracing, and data-transfer logic, then services become “fat” again. When cross-cutting concerns are duplicated across services:
  • Teams repeatedly solve identical operational problems in different ways.
  • Upgrading agents, certificates, or monitoring configurations becomes a coordination headache.
  • Developers must learn infrastructure and policy details unrelated to their business logic.
  • The intended agility of microservices is diluted by duplicated non-functional code.
These duplicated responsibilities are known as cross-cutting concerns. Without a strategy to centralize or offload them, the architecture drifts back toward complexity rather than achieving the modularity microservices promise.
An illustration titled "A Problem: Fat Microservices" showing four service boxes (Product Page, Details, Reviews, Ratings). Each box repeats common concerns—Authentication, Authorization, Networking, Logging, Monitoring, Tracing—and shows different language icons (Python, Ruby, Java, Node.js).

Additional operational challenges

Distributed microservices introduce additional complexity beyond duplicated cross-cutting code:
  • Service discovery and routing: determining which service version to call and how to split traffic for experiments.
  • Networking complexity: service-to-service communication needs timeouts, retries, circuit breakers, and routing policies.
  • Security: securing communication end-to-end and enforcing consistent authentication and authorization across services.
  • Observability: tracing requests across services requires consistent logging, metrics, and distributed tracing to pinpoint failures.
  • Operational overhead: heterogeneous tech stacks and many deployable units increase the burden on traditional ops models.
For small applications this already means multiple languages and tools; for larger organizations these problems can become overwhelming without new practices and platform support.
A presentation slide titled "Microservices – Limitations" showing four highlighted issues with icons: Complex service networking, Security, Observability, and Overload for traditional operation models.

Comparing architectures

A cultural shift: DevOps and shared responsibility

To succeed with microservices, teams commonly adopt DevOps practices—development and operations collaborate and share responsibility for deploying, monitoring, and operating services. Key practices include:
  • Automation (CI/CD)
  • Standardized tooling and runtime contracts
  • Platform teams that encapsulate infrastructure concerns
  • Observability as a built-in concern
  • Security and policy enforcement at the platform level
These practices reduce duplicated work and let teams focus on business functionality rather than re-solving operational challenges.
Refactoring a monolith into microservices is not only a code change—it requires organizational alignment, automation, and a platform that addresses cross-cutting concerns so teams can remain focused on core business logic.

Next steps

This article will continue by examining how service meshes and platform patterns can centralize or offload cross-cutting concerns—like traffic management, security, and observability—so that microservices remain small, focused, and operable.

References and further reading

  • Kubernetes Documentation
  • Introduction to service mesh concepts (search for “service mesh overview”)
  • Microservices architecture patterns and anti-patterns (search for “microservices best practices”)

Watch Video