The short version is: don't choose the messaging technology first. Choose the communication patterns your system actually needs, then pick the tool that fits those patterns.
A lot of monolith migrations become more complicated because teams introduce distributed systems problems before they actually need them.
Here's how I think about the three options.
| Feature | gRPC | NATS | Kafka |
|---|
| Primary model | Request/response RPC | Lightweight messaging | Durable event streaming |
| Latency | Excellent | Excellent | Good |
| Persistence | No | Optional (JetStream) | Yes |
| Ordering | Per connection | Limited | Strong per partition |
| Replay history | No | JetStream | Core feature |
| Operational complexity | Low | Medium | Highest |
| Best for | Synchronous APIs | Commands/events between services | Event sourcing, analytics, audit logs |
Use gRPC when services need answers immediately
Typical examples:
- User service asks Auth service to validate a token
- Checkout asks Inventory whether stock exists
- Pricing service computes a quote
Advantages:
- Simple mental model
- Strong typing (Protocol Buffers)
- Great tooling
- Easy debugging
- Excellent performance
Downside:
Every service becomes dependent on the availability of another.
This is fine when the dependency already exists logically.
Use NATS when you want asynchronous workflows
Example:
Order Created
↓
Inventory reserves stock
↓
Email sends confirmation
↓
Fraud checks order
↓
Analytics records purchase
Nobody blocks waiting for everyone else.
NATS is particularly attractive because:
- very low latency
- simple operational model
- request/reply when needed
- pub/sub
- queues
- JetStream adds durability
Many companies end up happier operating NATS than Kafka unless they truly need Kafka's persistence model.
Use Kafka when events become products
Kafka shines when multiple independent consumers need the same immutable history.
Example:
OrderPlaced
↓
Billing
↓
Shipping
↓
Analytics
↓
Machine Learning
↓
Fraud
↓
Data Lake
↓
Customer Timeline
The important feature isn't messaging.
It's replay.
If you deploy a new service tomorrow, it can replay years of history.
That's extremely powerful.
A common migration pattern
Instead of:
Monolith
↓
30 microservices
↓
Kafka everywhere
Successful migrations often look like:
Monolith
↓
Extract one service
↓
gRPC
↓
Extract another
↓
gRPC
↓
Need async events
↓
Introduce NATS
↓
Need historical replay
↓
Introduce Kafka
Notice the messaging system comes after the need appears.
Lessons from real migrations
1. Too many services too early
Teams often split by database tables.
Better:
Split by business capability.
Examples:
- Payments
- Orders
- Identity
- Inventory
Not:
- CustomerAddressService
- CustomerPhoneService
- CustomerNameService
2. Distributed transactions are painful
Inside a monolith:
Update inventory
Insert order
Charge card
Commit
One transaction.
Across services:
Inventory
↓
Payment
↓
Shipping
↓
Notification
Now failures happen in the middle.
Expect to use:
- Saga patterns
- Idempotency
- Retries
- Compensation logic
3. Start synchronous, then remove coupling
Many teams try to make everything event-driven.
Then discover simple queries become difficult.
A practical rule:
- Queries → gRPC
- Commands → gRPC or messaging
- Domain events → messaging
4. Avoid shared databases
This is probably the biggest architectural mistake.
Instead of
Service A
↓
Shared Database
↑
Service B
prefer
Service A owns its database
↓
API/events
↓
Service B
Otherwise you've recreated a distributed monolith.
5. Version everything
Events never stay the same.
Expect:
OrderCreated v1
OrderCreated v2
OrderCreated v3
Consumers evolve at different speeds.
Protocol Buffers (for gRPC) and schema management for events help maintain compatibility.
6. Expect eventual consistency
Developers coming from monoliths often expect:
User clicks Buy
↓
Everything updates instantly
Distributed systems usually become:
Order accepted
↓
Inventory updates
↓
Payment confirms
↓
Email sends
↓
Search index updates
↓
Analytics updates
Design UIs and business processes around this reality.
7. Observability becomes essential
In a monolith, a stack trace often tells the whole story.
With multiple services, you'll want:
- Distributed tracing (for example, using OpenTelemetry)
- Centralized logs
- Metrics
- Correlation IDs that follow requests and events
Without them, debugging becomes significantly harder.
What I'd recommend for most teams
If you're extracting services from a typical CRUD/business application:
- Use gRPC for synchronous service-to-service calls.
- Introduce NATS when you need asynchronous workflows, background processing, or fan-out events.
- Add Kafka only if you have a clear need for durable event streams, replay, auditability, or large-scale data integration.
That progression keeps operational complexity low while leaving room to grow.
If you can share a bit more about your system—its size (for example, number of developers and services), expected traffic, and domain (e-commerce, fintech, SaaS, IoT, etc.)—I can suggest an architecture that's better tailored to your migration rather than a one-size-fits-all recommendation.