DEV Community

Victor Adebayo
Victor Adebayo

Posted on

Microservices Aren't Just About Breaking a Monolith Into Smaller Apps

Recently, I collaborated with some incredible developers Damilare Ogundele, Gabriel Michael, Joshua Joseph to build an AI-driven content platform.

The platform included several capabilities:

  • User management
  • Blog publishing
  • Background job processing
  • Analytics
  • AI-powered workloads

We decided to explore a polyglot microservices architecture, using Python for some services and TypeScript for others.

For communication, we used gRPC for synchronous service-to-service communication and RabbitMQ for asynchronous processing.

On paper, everything looked great.

Different repositories.
Independent services.
Independent deployments.
Different technologies for different problems.

It felt like we were building something similar to the architectures used by large-scale engineering teams.

Then I deleted and rebuilt the repository.

Twice.

Not because the code was bad.

My thinking about the architecture was wrong.


The Common Microservices Misconception

One of the most common explanations of microservices is:

"Take your monolith and break it into smaller applications."

It sounds reasonable.

But if you're not careful, you don't end up with microservices.

You end up with a distributed monolith.

For example:

Monolith
   |
   +---- User Service
   |
   +---- Payment Service
   |
   +---- Notification Service
   |
   +---- Analytics Service
   |
   +---- Fraud Service
Enter fullscreen mode Exit fullscreen mode

Technically, these are separate applications.

But if every service depends heavily on the others, you've simply moved the complexity from one codebase into the network.

The better question isn't:

How many services do we have?

The better question is:

What business capability does each service own?

That changed the way I approached the rebuild.


Think in Business Capabilities

Imagine a fintech platform responsible for:

  • Authentication
  • Payments
  • Fraud detection
  • Transaction notifications
  • Analytics

A poor service boundary might look like this:

User Service
Transaction Service
Payment Service
Notification Service
Database Service
Enter fullscreen mode Exit fullscreen mode

Now every service needs to communicate with several other services.

Instead, we should think in terms of business capabilities.

                    API Gateway
                         |
          +--------------+--------------+
          |              |              |
          v              v              v
   Authentication     Payments      Analytics
          |              |
          |              +----> Fraud Detection
          |
          +----> User Management

                         |
                         v
                      RabbitMQ
                         |
                 +-------+-------+
                 |               |
                 v               v
          Notifications    Background Jobs
Enter fullscreen mode Exit fullscreen mode

The exact boundaries depend on the domain.

The important part is that each service should have a meaningful responsibility.

A service shouldn't exist just because a database table or class exists.


What Went Wrong in My First Architecture

My first instinct was to start with technologies.

I was thinking:

  • "This service can use Python."
  • "That service can use TypeScript."
  • "Let's use gRPC."
  • "Let's introduce RabbitMQ."

These are all reasonable technologies.

But they shouldn't be the starting point.

The better questions are:

  • What are our core business capabilities?
  • Who owns each capability?
  • What data does each service own?
  • Which operations need synchronous communication?
  • Which operations can be asynchronous?
  • What happens when a dependency fails?
  • Which workloads actually need to scale independently?

Only after answering those questions should we start making technology decisions.


Service Ownership Matters

One of the biggest lessons from the rebuild was:

A service should own its business capability end-to-end.

That includes its:

  • Business logic
  • Data
  • APIs
  • Events
  • Operational responsibilities

For example:

Payment Service
├── Payment business logic
├── Payment database
├── Payment APIs
└── Payment events
Enter fullscreen mode Exit fullscreen mode

Rather than:

Payment Service
      |
      v
Shared Database
      ^
      |
Fraud Service
Enter fullscreen mode Exit fullscreen mode

Shared databases can create hidden coupling.

You may have independent applications, but if multiple services directly modify the same database tables, your architecture is still tightly coupled.


gRPC Is Not a Magic Boundary

We used gRPC for synchronous internal communication.

One of its biggest advantages is having strongly typed service contracts through Protocol Buffers.

A simplified architecture could look like:

Content Service
      |
      | gRPC
      v
User Service
      |
      | gRPC
      v
Authorization Service
Enter fullscreen mode Exit fullscreen mode

But there's an important lesson here:

A strongly typed protocol doesn't automatically create a good architecture.

You can still build a highly coupled system using gRPC.

If Service A cannot work whenever Service B is unavailable, the problem isn't the RPC framework.

The problem is the dependency relationship.


Version Your Contracts

Once services communicate through APIs, those APIs become contracts.

Changing a field casually can break downstream consumers.

For example:

v1 PaymentRequest

amount
currency
user_id
Enter fullscreen mode Exit fullscreen mode

Changing user_id to customer_id may seem harmless.

But if five services consume that contract, you've created a compatibility problem.

This is why API compatibility, contract testing, and versioning become increasingly important as the system grows.


RabbitMQ for Asynchronous Processing

Not every operation needs to happen during the user's HTTP request.

For long-running or background workloads, we used RabbitMQ.

A simplified flow looks like this:

User Request
     |
     v
Content Service
     |
     | Publish Job
     v
 RabbitMQ
     |
     v
Worker Service
     |
     v
AI Processing
     |
     v
Result/Event
Enter fullscreen mode Exit fullscreen mode

This allows expensive workloads to happen asynchronously instead of blocking the user request.

But we learned another important lesson:

A message broker doesn't eliminate coupling. It changes how coupling happens.

Queues and events still need clear ownership.


Domain Events

Instead of creating arbitrary shared queues, services can publish meaningful domain events.

For example:

Payment Service
      |
      | PaymentCompleted
      v
    RabbitMQ
      |
      +----> Notification Service
      |
      +----> Analytics Service
      |
      +----> Fraud Service
Enter fullscreen mode Exit fullscreen mode

The Payment Service owns the event.

Other services consume it when they need it.

This creates a cleaner relationship between services.


Failure Is Not an Edge Case

This was probably the biggest mindset shift for me.

In distributed systems, failure is normal.

A service can be:

  • Down
  • Slow
  • Overloaded
  • Temporarily unreachable
  • Returning errors
  • Processing messages slowly

Consider:

API Gateway
     |
     v
Payment Service
     |
     v
Fraud Service
Enter fullscreen mode Exit fullscreen mode

What happens if Fraud Service takes 30 seconds to respond?

Does the payment request also take 30 seconds?

What happens if Fraud Service is completely unavailable?

Do we retry? How many times? For how long?

These aren't just implementation details.

They're architectural decisions.


Timeouts, Retries and Circuit Breakers

Every synchronous dependency should have a reasonable timeout.

Without timeouts, one slow service can cause other services to wait indefinitely.

Request
  |
  v
Service A
  |
  v
Service B  <-- slow
  |
  v
Service C  <-- waiting
Enter fullscreen mode Exit fullscreen mode

Retries can help with temporary failures, but they need to be used carefully.

A system that blindly retries everything can create a retry storm.

Good distributed systems often combine:

Timeouts
   +
Retry limits
   +
Exponential backoff
   +
Circuit breakers
   +
Idempotency
Enter fullscreen mode Exit fullscreen mode

The goal isn't to prevent every failure.

The goal is to contain failure.


Observability Becomes Critical

Debugging a monolith can already be difficult.

Debugging a distributed system can be much harder.

A single request might travel through:

Client
  |
  v
API Gateway
  |
  v
Content Service
  |
  v
User Service
  |
  v
RabbitMQ
  |
  v
Worker
  |
  v
AI Service
Enter fullscreen mode Exit fullscreen mode

When something fails, we need to answer questions like:

  • Which service failed?
  • Which request caused it?
  • What was the correlation ID?
  • How long did each service take?
  • Was the failure synchronous or asynchronous?
  • Was a message retried?
  • Where did the request originate?

This is why distributed systems need strong:

  • Logging
  • Metrics
  • Tracing
  • Correlation IDs
  • Health checks

Microservices Have a Cost

This is something I underestimated.

Microservices don't remove complexity.

They move complexity from the codebase into the system.

With a monolith:

Code complexity
Enter fullscreen mode Exit fullscreen mode

With microservices:

Code complexity
+
Network complexity
+
Deployment complexity
+
Data consistency
+
Observability
+
Failure handling
+
Contract management
+
Infrastructure
Enter fullscreen mode Exit fullscreen mode

That's a significant trade-off.

For a small application, a modular monolith can often be a better starting point.

You can still have strong boundaries:

Application
├── Users
├── Content
├── Notifications
├── Analytics
└── Jobs
Enter fullscreen mode Exit fullscreen mode

And extract a module into a service later when there is a genuine reason to do so.


When Should You Split a Service?

Some useful signals include:

1. Independent scaling

One workload needs significantly more resources.

API:          2 instances
AI Workers:  20 instances
Analytics:    4 instances
Enter fullscreen mode Exit fullscreen mode

2. Independent deployment

A capability changes frequently and shouldn't require redeploying the entire application.

3. Strong business boundary

The capability has a clear domain and ownership model.

4. Different infrastructure requirements

For example, AI workloads may eventually require GPU resources while other services don't.

5. Independent team ownership

A team can build, deploy, monitor, and operate the service independently.


What I Would Do Differently Today

If I started the project again, I wouldn't begin by creating multiple repositories.

I'd begin with the domain.

Understand Domain
       ↓
Identify Business Capabilities
       ↓
Define Ownership
       ↓
Define Data Boundaries
       ↓
Define Communication
       ↓
Define Failure Behaviour
       ↓
Identify Scaling Requirements
       ↓
Choose Service Boundaries
       ↓
Choose Technologies
Enter fullscreen mode Exit fullscreen mode

Rather than:

Choose Technologies
       ↓
Create Services
       ↓
Figure Out Boundaries
Enter fullscreen mode Exit fullscreen mode

That difference is huge.


The Real Definition of Microservices

My understanding of microservices has changed.

Today, I'd define them as:

Independently deployable services organized around clear business capabilities, with explicit ownership, stable contracts, controlled communication, and failure isolation.

The goal isn't:

More services = Better architecture
Enter fullscreen mode Exit fullscreen mode

The goal is:

Clear boundaries
       +
Independent ownership
       +
Independent deployment
       +
Controlled communication
       +
Failure isolation
       =
A system that can evolve safely
Enter fullscreen mode Exit fullscreen mode

The biggest lesson from rebuilding our platform wasn't whether we should use Python, TypeScript, gRPC, or RabbitMQ.

It was this:

Architecture starts with boundaries, not technologies.

And sometimes the best first step toward microservices is not creating a microservice at all.


Final Thoughts

Building microservices can be exciting.

Multiple repositories. Different languages. gRPC. Message brokers. Containers. Independent deployments.

It feels like you're building something sophisticated.

But sophistication isn't the same thing as good architecture.

The difficult part isn't splitting an application.

The difficult part is deciding where the boundaries should actually be and designing those boundaries so that the system can continue working when individual components fail.

If you've built microservices systems at scale, what architectural lesson took you the longest to learn?

I'd especially love to hear about your experiences with:

  • Service boundaries
  • Event-driven architecture
  • Distributed transactions
  • Observability
  • Failure handling

Let's learn from each other.


Technologies: Python, TypeScript, gRPC, RabbitMQ, Microservices, API Gateway, AI, Distributed Systems

Team: Built collaboratively with Damilare Ogundele, Gabriel Michael, Joshua Joseph

Top comments (0)