When Should You Actually Use Microservices? Lessons From Building a Real Project

Microservices are everywhere in modern software architecture discussions. When starting a new project, it is easy to assume that breaking everything into separate services automatically makes the system better. But after working with a real system, I learned that Microservices are not simply a better version of a monolith. They solve certain problems, but they also introduce a new set of challenges. The real question is not, "Should I use Microservices?" but rather, "Does my project actually need Microservices?"
Why We Chose Microservices
In one of my recent projects, we decided to use a Microservices architecture because the system had several different domains with different responsibilities. Instead of building everything inside one application, we separated major areas into independent services. A simplified architecture looked something like this:

The idea was to make each service responsible for a specific business domain. For example, authentication should not need to know how marketplace products are managed. Similarly, a social feature should not directly depend on the internal implementation of the trading system. This separation gives us clearer boundaries between different parts of the application and makes it easier to reason about the responsibilities of each component.
The Benefits We Experienced
Independent Development
One of the biggest advantages of Microservices is that different teams or developers can work on different services without constantly modifying the same codebase. Each service has a clear responsibility, which can reduce the chance of unrelated changes affecting other parts of the system. It also allows teams to develop and deploy certain components more independently when the architecture and infrastructure support it.
Independent Scaling
Not every part of an application has the same traffic pattern or resource requirements. For example, a service responsible for real-time market data may require significantly more resources than a less frequently used administrative service. With Microservices, individual components can potentially scale independently instead of scaling the entire application together.
Clear Domain Boundaries
A well-designed Microservices architecture encourages developers to think carefully about business domains. Instead of creating a large application where everything depends on everything else, each service has a specific responsibility. When the boundaries are designed correctly, this can make a complex system easier to understand and evolve over time.
But Microservices Add Complexity
This is the part that is sometimes overlooked. When everything is inside one application, calling another module can be as simple as calling a function. With Microservices, communication may look like this:

Now we have to think about network failures, timeouts, retries, authentication between services, service discovery, message delivery, logging, monitoring, and distributed tracing. The system becomes more flexible, but also more complicated. A failure that would previously be a simple exception inside one process can now involve multiple services and network boundaries.
Debugging Is Different
One of the biggest changes I noticed is debugging. In a monolithic application, you might see a relatively straightforward flow:

With Microservices, the same request could travel through multiple components:
Client ↓ API Gateway ↓ Service A ↓ Message Broker ↓ Worker ↓ Service B ↓ Database
When something goes wrong, finding the root cause requires better observability. Without centralized logs, correlation IDs, metrics, and distributed tracing, debugging can become extremely difficult. This is why observability should not be treated as an optional feature in a Microservices architecture. The more distributed the system becomes, the more important it is to understand what is happening across the entire request flow.
Communication Becomes a Design Problem
Another challenge is deciding how services should communicate. Should we use REST, gRPC, RabbitMQ, or Google Cloud Pub/Sub? The answer depends heavily on the use case and the requirements of each service.
Synchronous communication is often easier to understand:
Service A → HTTP → Service B
However, Service A may become dependent on Service B being available at that exact moment. Asynchronous messaging can reduce direct coupling:
Service A → Message Broker → Service B
But now we need to consider duplicate messages, message ordering, retries, dead-letter queues, and idempotency. Microservices do not remove complexity. They often move complexity from the application code into the architecture and infrastructure.
Database Ownership Is Important
One principle I found particularly important is database ownership. Ideally, a service should own its data rather than allowing every service to directly access the same database tables. For example:
Auth Service ↓ Auth Database
Marketplace Service ↓ Marketplace Database
Social Service ↓ Social Database
This creates stronger boundaries between services. However, it also makes cross-service queries more complicated. In a monolith, joining two tables might be straightforward. In a Microservices architecture, you may need to combine data from multiple services through APIs, events, or replicated data. Again, this is a trade-off rather than a universal improvement.
When Microservices Make Sense
Based on my experience, Microservices can be a good choice when the system has clearly separated business domains, different components need to scale independently, multiple teams work on different parts of the system, some services have significantly different infrastructure requirements, or the system is expected to grow substantially. Microservices can also make sense when the team has enough experience with distributed systems and is prepared to invest in deployment automation, monitoring, logging, and operational tooling. In these situations, the additional complexity may be justified by the benefits.
When a Monolith May Be Better
For a small application, Microservices can easily become unnecessary complexity. If your application has a small team, a simple domain, low traffic, few deployment requirements, and no need for independent scaling, a modular monolith may be a better starting point.
The application remains a single deployable unit, but the internal boundaries are clear. Later, if one module becomes large enough or develops different scaling and operational requirements, it can potentially be extracted into a separate service. This approach allows the architecture to evolve based on actual needs rather than assumptions made at the beginning of the project.
My Current View
After working with Microservices, I no longer see the architecture as something that should be chosen simply because it is modern or popular. Microservices are a tool. They are useful when the problem requires them, but they are not automatically better than a monolith.
The most important lesson I learned is that architecture should follow the problem, not the trend. If a project needs independent scaling, strong domain boundaries, multiple teams, or different infrastructure requirements, Microservices can be a powerful approach. But if the project is small and simple, introducing distributed systems too early may create more problems than it solves.
The best architecture is not the one with the most services. It is the one that makes the system easier to build, operate, understand, and evolve. That is the perspective I now try to keep in mind whenever I design a new system.
All rights reserved