The Problem with Microservices
How to keep a really good idea from going badly wrong
Microservices promise agility, scalability, and resilience, but too many applications fail to realize these benefits due to fundamental issues with how services are defined and connected. So how can we fix that?
Poorly drawn boundaries lead to tightly coupled services, making a system brittle and difficult to change. While at the same time, static configurations and rigid dependencies hinder the flexibility needed to meet dynamic and evolving business, deployment, and operational requirements.
These problems often result in bloated applications where services duplicate functionality, workflows are fragile, and communication becomes a bottleneck. In Yes… Microservices REALLY ARE Technical Debt , Dave Farley describes the reasons for — and consequences of — these problems.
At their core, these challenges stem from two interconnected issues — the failure to minimize destructive coupling between services — and the lack of a flexible, scalable model for configuring and connecting them.
Addressing these issues is necessary to unlock the full potential of microservices applications.
Effective Microservices: Boundaries and Connections
In software development, building robust, scalable, and adaptable systems often comes down to how well we design and integrate the services that make up these systems. Two key considerations are:
- How we define boundaries around individual services to avoid destructive coupling.
- How we configure and connect those services to foster dynamic adaptability.
These considerations are especially important for microservices development — because poorly designed boundaries and connections can easily undermine the promised agility and maintainability of microservices.
In this article we’ll try to address these challenges, highlighting how domain-driven design (DDD), standardized bindable interfaces, and dynamic messaging models enable scalable, resilient service-based systems.
The Challenge of Defining Service Boundaries
Services with poorly defined boundaries often lead to systems that are brittle, tightly coupled, and expensive to change. When one service accesses another service, any changes to the latter can ripple across the system, requiring updates in multiple places.
Examples of Destructive Coupling: Imagine a payment processing service directly invoking methods on an inventory service to update stock levels after a successful purchase. If the inventory service changes its API or internal logic, the payment service might break. Worse, if the inventory service becomes unavailable, the payment process may also fail, even though the two responsibilities are logically distinct.
Domain-Driven Design as a Solution: Domain-driven design (DDD) offers a solution by encouraging developers to model services as bounded contexts and aggregates of contexts. A bounded context defines a clear boundary around a specific domain within the application. Each context encapsulates related entities, value objects, and aggregates, ensuring that interactions with other parts of the system are implicit and governed by well-defined contracts.
In an e-commerce platform, the payment and inventory services are modeled as separate bounded contexts, each responsible for its own domain. Interactions between them are mediated through implicit interfaces rather than direct dependencies, reducing the risk of ripple effects and enhancing modularity.
Configuring and Connecting Services: Dynamic Composition
Traditional approaches to connecting services often rely on static configurations or dependencies. This approach limits flexibility, as changes to one service might require extensive reconfiguration or redeployment of others.
Integrating a new shipping service into an order management system might involve manually updating configuration files, modifying code, and redeploying the application. This necessity not only slows down innovation but also introduces opportunities for errors.
The Power of Bindable Interfaces: A bindable interface provides a standardized — and implicit — contract for service interaction. All services expose a uniform set of methods aligned with HTTP methods — such as GET, POST, REPLACE, and DELETE. These interfaces align with RESTful principles, leveraging concepts like statelessness and resource orientation to simplify communication.
Bindable interfaces allow services to be dynamically connected and dynamically scaled at runtime. Instead of hard coding connections, services can discover and bind to each other on-the-fly.
So, when a shipping service is added, it can register itself with the system at runtime, making its services immediately available without requiring changes to existing code.
Dynamic Composition in Practice: Consider a workflow where an order management system coordinates between inventory, payment, and shipping services.
Using bindable interfaces, a message moderator can dynamically discover available matching services, bind to them, and organize the workflow without needing predefined connections — and the service being bound can register its preferences (topic subscriptions, acceptable messaging downgrades under load, etc.) with the moderator.
Messaging Models for Decoupled Communication
Limitations of Traditional Messaging: Many systems rely on synchronous messaging with tightly coupled patterns, which can create bottlenecks and limit scalability — when, if an immediate response is not required, it could use asynchronous messaging to minimize bottlenecks and to scale service instances dynamically. Sometimes a message must be synchronous, sometimes it doesn’t — so keep your options open.
Dynamic Messaging through Moderation: A message moderator introduces a flexible, dynamic approach to messaging. There is a message moderator per service node.
Within the same service node, messages between services are implemented as standard method calls. Only messages that cross between service nodes are transformed into network calls.
Acting as an intelligent intermediary, it dynamically routes messages based on their targeted service and runtime conditions. This approach supports multiple communication patterns, including:
- Synchronous messaging: Immediate request/response workflows. When an immediate response is needed a service can send a synchronous message and wait for a response before proceeding
- Asynchronous messaging: Decoupled interactions where senders and receivers operate independently. When a service does not need a response before proceeding, it can post an asynchronous message and proceed without waiting.
- Event-driven communication: Publishing and subscribing to events for real-time system coordination. When a service publishes an event it can immediately proceed with the understanding that interested subscribers will receive the event message.
Dynamic Messaging and Resilience: The moderator also enhances resilience. If a local instance of a service is temporarily unavailable, or response times too degraded, a moderator can automatically route the message to another available instance of the same service on another service node, or if acceptable to the sender, queue the message for later delivery.
Additionally, by validating, signing, and encrypting messages — and logging messages and responses — the moderator reliably strengthens security and observability.
Holistic Approach to Application Design
Unified Composition and Messaging: The combination of bindable interfaces and dynamic messaging enables a unified approach to application design. Services can be dynamically composed into workflows while communicating seamlessly through moderated messages. This reduces the overhead of manual configuration and ensures that the system can adapt to new requirements more easily.
Practical Modularity: By adhering to the principles of DDD and leveraging standardized interfaces, developers can design truly modular systems. Clear boundaries between services reduce the risk of destructive coupling, while dynamic composition enables services to work together in flexible, scalable ways.
Implicit Workflows: Combines aggregates, messaging, and event publishing to coordinate application workflows across bindable components. For instance: 1) When a new order is created, a message is sent to the Order aggregate, which validates the order and publishes an event for the next step in the workflow. 2) Each bindable service (e.g., inventory, shipping, billing) subscribes to these events and processes them independently, allowing the workflow to progress dynamically without hard-coding dependencies.
Real-World Scalability: Dynamic messaging supports distributed architectures, allowing systems to scale horizontally across nodes. For example, an order management system can operate across multiple service nodes, with the message moderator ensuring that messages are routed efficiently between nodes.
Enhanced Developer Productivity: By abstracting the complexities of configuration and messaging, developers can focus on business logic rather than low-level integration details. This leads to faster development cycles, fewer errors, and a better alignment between application and business requirements.
Wrapping Up
The effectiveness of a microservices application lies in how well it defines boundaries and manages connections. By leveraging domain-driven design for decomposition, adopting bindable interfaces for standardized interactions, and employing dynamic messaging models for flexibility, modern microservices architectures can overcome the limitations of traditional approaches.
These principles not only enable more adaptable and scalable systems but also empower developers to create applications that are better aligned with business goals.
Whether you’re building a new application or modernizing an existing one, focusing on boundaries and connections is key to long-term success in service-based application design.
Thanks for reading!
Questions and comments are always welcome.
If you found this useful, a clap would let us know we’re on the right track.
Some more reading on message moderation.