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
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
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
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
Rather than:
Payment Service
|
v
Shared Database
^
|
Fraud Service
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
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
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
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
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
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
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
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
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
With microservices:
Code complexity
+
Network complexity
+
Deployment complexity
+
Data consistency
+
Observability
+
Failure handling
+
Contract management
+
Infrastructure
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
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
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
Rather than:
Choose Technologies
↓
Create Services
↓
Figure Out Boundaries
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
The goal is:
Clear boundaries
+
Independent ownership
+
Independent deployment
+
Controlled communication
+
Failure isolation
=
A system that can evolve safely
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)