Microservices are supposed to simplify systems, but what happens when they don't? We've seen systems crumble under the weight of poor design choices, and it's time to acknowledge the anti-patterns that plague our industry.
One of the worst outcomes of a microservices migration is the distributed monolith: services that are locked together because they share a database, a domain model or have synchronous dependencies that make independent deployment impossible. This setup inherits all the operational overhead of distributed services without the deployment independence that justifies it.
I've seen this happen with a team that built a recommendation engine as a separate microservice, but then tightly coupled it to a product catalog service that used the same database schema. Any update to the product catalog required a corresponding update to the recommendation engine, making independent deployment a nightmare.
When a microservice makes 15 synchronous calls to other services per request, creating latency chains, tight coupling and cascading failure paths, it's time to reevaluate the design. This chatty service problem arises when services are not designed to operate independently, and a single user request triggers a long chain of service-to-service calls.
We used to have a service that made 20 synchronous calls to other services, and it would take around 5 seconds to complete a single request. We had to optimize this service to reduce the number of calls, and we were able to bring it down to 2 calls, reducing the latency to under 1 second. It was a huge win for our users.
Sharing a database between multiple services is another common anti-pattern. Any change to the shared schema requires coordinating all dependent services, and the services can't deploy independently because their data contracts are tightly coupled. The database becomes a hidden dependency that undermines the loose coupling microservices promise.
Decomposing a system into microservices before understanding the domain can lead to brittle service boundaries. Every new feature requires changes to multiple services because the boundaries don't align with actual change patterns. This premature decomposition is a recipe for disaster, making it harder to merge services in the future.
We've been guilty of this ourselves, and it's not easy to untangle the mess. One of the hardest things is figuring out which data goes in which database, and how to handle conflicts when multiple services need to access the same data. It's a classic problem of domain-driven design vs. technical debt.
The problem with premature decomposition is that it reflects organisational structure or personal preference rather than stable domain boundaries. When this happens, merging services becomes a painful process involving state migration and deployment coordination, often more painful than the original monolith.
One project I worked on had 10 microservices that were all tightly coupled to each other, and we needed to merge two of them into a single service. It took us 6 months of continuous effort, and we still haven't fully resolved the issues. It's a hard lesson learned, but one that we'll carry with us for the rest of our careers.