Why Microservices Matter
Why Microservices Matter
The Problem: A successful product grows. The codebase grows with it. Eventually a single deploy ships the work of fifty engineers, a one-line bug in checkout means the whole site rolls back, and the team that needs more CPU for video transcoding is stuck waiting for the team that wants to refactor the email templates.
The Solution: Split the system into small, independently deployable services that each own one capability. Teams ship at their own cadence. The transcoder scales without dragging the email templates along. A bug in one service does not roll back the others.
Real Impact: Amazon went from one deploy every few weeks to thousands of deploys per day. Netflix runs hundreds of services and a bad recommendation does not stop you watching. Uber runs over 2,000 services to keep dispatch decisions under a second across 10,000 cities.
Real-World Analogy
Think of a restaurant kitchen. The grill, the salad station, the dessert station, the bar — each has its own equipment, its own ingredients, its own chef who knows the craft. When the grill breaks, the kitchen still serves salads, drinks, and desserts. When the dessert chef quits, you replace one person, not the whole brigade.
A monolith is one chef trying to do every station from one workbench. It works fine for a small cafe. It fails as soon as you have more than one customer per minute or more than one chef who wants to work in the kitchen.
Most engineers meet microservices through hype. The clean version of the story is simpler: microservices are a way of cutting an application along team and capability lines so that the cost of growing the system stays roughly linear in the number of teams, instead of growing exponentially. Whether they are right for your system depends entirely on whether you have that growth problem or are merely anticipating it.
The Reframe That Helps
A microservice is not a small program. It is an independently deployable unit owned by one team. The size question (“how small is small?”) is the wrong question. The right question is: can one team ship this on its own schedule, run it on its own infrastructure, and rewrite it without coordinating with anyone else? If yes, it is a microservice. If no, it is something else — usually a distributed monolith pretending to be microservices.
Monolith vs Microservices
The honest comparison starts with admitting that monoliths are not bad. They are the right shape for most applications most of the time. Microservices are what you reach for when a monolith stops fitting — not before.
The shape of a monolith
ecommerce-monolith/
├── app.py # all logic in one process
├── models/
│ ├── user.py
│ ├── product.py
│ ├── order.py
│ └── payment.py
├── database/
│ └── single_database.db # one schema for everything
└── templates/
One process. One deploy. One database. One language. One build. That is also one set of eyes on every change, one rollback for every bug, and one team holding the line on every refactor.
The shape of a microservice mesh
ecommerce/
├── user-service/ # owns users + sessions + auth
│ └── postgres
├── product-service/ # owns the catalog
│ └── postgres
├── cart-service/ # owns shopping carts
│ └── redis
├── order-service/ # owns orders + workflow
│ └── postgres
├── payment-service/ # owns charges + refunds
│ └── postgres
├── shipping-service/ # owns rates + tracking
│ └── postgres
└── notification-service/ # owns email + SMS + push
└── kafka topic
Each service owns its data. Each speaks to others over the network. Each can be written in a different language, deployed at a different cadence, and scaled to a different size. That is the upside. The downside is that you now have a network in the middle of every former function call.
Side by side
| Aspect | Monolith | SOA | Microservices |
|---|---|---|---|
| Size | One large application | A few large services | Many small services |
| Deployment | Deploy the whole app | Deploy service groups | Deploy each service independently |
| Scaling | Scale the whole app | Scale service groups | Scale per service |
| Tech stack | One stack everywhere | Usually one stack | Polyglot — pick per service |
| Database | One shared schema | Often shared | One per service |
| Communication | In-process function calls | Enterprise Service Bus | HTTP, gRPC, async messaging |
| Team shape | One large team | Teams per service group | Small autonomous teams |
| Failure blast radius | Whole system down | Whole subsystem down | Isolated to one service (in theory) |
| Time to first deploy | Hours | Days | Weeks — you have to build infra first |
| Operational cost | Low | Moderate | High — you are running a distributed system |
| Testing | Test the whole app | Integration-heavy | Per-service + contract + end-to-end |
| Best fit | Small apps, MVPs, single team | Enterprise integration | Many teams, large scale, cloud-native |
The pattern across the rows
Microservices trade local simplicity (one process, one DB, one deploy) for organizational simplicity (one team, one schedule, one rewrite). If your bottleneck is local — you only have a few engineers and one product — you trade the wrong way and end up paying the operational cost of a distributed system to solve a problem you do not have.
The Defining Characteristics
Strip away the marketing and a microservice has four traits that make it a microservice. Miss any one of them and you have a different animal — usually a distributed monolith.
1. Single, well-defined responsibility
A microservice does one thing. The Payment service charges cards. The Catalog service answers questions about products. The Notification service sends email and SMS. If you cannot describe what a service does in one sentence without using the word “and,” the boundary is wrong.
2. Independently deployable
You can ship the Payment service at 2 PM on a Tuesday without coordinating with anyone. The Catalog team can roll back without affecting checkout. If a deploy of one service requires a deploy of another, they are one service wearing two costumes.
3. Owns its data
The Payment service is the only thing that talks to the payment database. The Catalog service is the only thing that talks to the catalog database. Other services ask over the network — they do not reach into the database directly. The moment two services share a schema you have lost your independence and gained a coordination problem with extra latency.
4. Loosely coupled, communicates over the network
Services talk through APIs (HTTP, gRPC) or messages (Kafka, SQS). They do not link in each other’s code. The contract between them is the API, not the implementation. You can rewrite the Catalog service in Go on Monday and as long as the API still answers the same way, every other service is unaffected.
The most common failure mode
The pattern most teams ship and call “microservices”: many small services, but they all share one database. That is a distributed monolith. You pay the operational cost of microservices — networks, deploys, observability — and get none of the independence, because every schema migration still needs every team to coordinate. If services share a database, you have a monolith with extra latency.
A simple service in three languages
The shape is the same in every stack: an HTTP handler, a model, and one job done well. The choice of language is part of what you get to decide per service.
# Python (Flask) — a Product service
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/products/<int:product_id>")
def get_product(product_id):
product = {"id": product_id, "name": "Laptop", "price": 999.99}
return jsonify(product)
if __name__ == "__main__":
app.run(port=5001)
// Java (Spring Boot) — same Product service
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired private ProductService productService;
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
return ResponseEntity.status(HttpStatus.CREATED).body(productService.save(product));
}
}
// Go — a Cart service
package main
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
type CartItem struct {
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
}
type Cart struct {
UserID string `json:"user_id"`
Items []CartItem `json:"items"`
}
func getCart(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
cart := Cart{
UserID: vars["userID"],
Items: []CartItem{{ProductID: "123", Quantity: 2, Price: 29.99}},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cart)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/cart/{userID}", getCart).Methods("GET")
http.ListenAndServe(":5003", nil)
}
When Microservices Are the Right Choice
The honest checklist. If you can answer yes to most of these, microservices will probably make your life better. If you cannot, they almost certainly will not.
| Signal | What to look for | Why it matters |
|---|---|---|
| Team size | 10+ engineers across multiple teams | Below this, the coordination cost of microservices is higher than the coordination cost of a monolith. |
| Deployment frequency | You want to deploy multiple times a day | One deploy pipeline gating fifty engineers stops working at this cadence. |
| Differential scaling | One feature needs 10x the capacity of others | Image processing vs auth: you should not pay to scale auth to handle image traffic. |
| Tech diversity matters | ML in Python, real-time in Go, finance in Java | A monolith forces everything into one runtime. |
| Domain has clear boundaries | E-commerce, fintech, marketplaces — capabilities are obvious | You cannot draw service lines you cannot draw on a whiteboard first. |
| DevOps maturity exists | CI/CD, monitoring, IaC are already in place | Microservices make every operational gap more painful, not less. |
| Teams are autonomous | Teams can decide what to ship and when | Independent deploys require independent decision-making. |
| You can debug distributed systems | Tracing, structured logging, runbooks are normal here | Distributed bugs are real bugs and require real tooling. |
The one-line test
If your hardest bottleneck this quarter is “we cannot deploy fast enough because too many teams share one pipeline,” microservices help. If it is “we cannot ship features fast enough because we are still figuring out the product,” they hurt.
When Microservices Are the WRONG Choice
Do not start a project as microservices
Both Amazon and Netflix started as monoliths and migrated to microservices only when scale and team size demanded it. Starting with microservices on day one is a confident statement that you know exactly what your service boundaries are — before you have shipped a single thing to a real user. You almost certainly do not. Service boundaries you draw before discovery will be wrong, and they will be expensive to redraw.
- Team size: Fewer than 5 engineers? Stay monolithic. The whole team can read the whole codebase.
- Traffic: Under 1,000 requests per second? A boring monolith will handle it on one box with room to spare.
- Org maturity: No CI/CD, no on-call rotation, no monitoring? Fix that first — microservices will magnify every gap.
- Product market fit: Still iterating on what the product even is? You will move faster as a monolith and rewrite cheaper, too.
The cost the marketing leaves out
Every microservice you ship adds a recurring tax: a deploy pipeline, a service registry entry, a dashboard, alerts, on-call ownership, an SLO, contract tests, runbooks. Twenty services is twenty of each of those. The cost is not in writing the services. It is in operating them, every day, forever.
The pre-flight check
Before you commit to the migration, write down honest answers to these:
- Can you draw the service map — capability by capability — on a whiteboard right now? If not, it is too early.
- Do you have a runbook for what happens when a service goes down at 3 AM? If not, you do not have an on-call story.
- Do you have distributed tracing? Without it, debugging a failed checkout that touches eight services is a forensic exercise.
- Do you have one team per planned service, or are two services going to be owned by the same five people? If the latter, merge them.
Building Blocks of a Microservice
A production microservices architecture is not just “some services.” It is the services plus the supporting infrastructure that lets them find each other, route traffic safely, and stay alive when something goes wrong.
API Gateway
A single entry point for outside traffic. The gateway handles routing, authentication, rate limiting, request shaping, and translation between protocols (REST in, gRPC out). Without a gateway, every client has to know about every service. With one, the topology behind it can change without breaking clients.
Client → API Gateway → [Auth Service, User Service, Product Service, ...]
Cross-cutting concerns the gateway owns:
authentication and token verification
rate limiting and quota enforcement
request logging and tracing context
protocol translation (REST <-> gRPC)
response caching for safe GETs
Service discovery
In a static world, services know each other’s addresses. In a real world — pods come and go, deploys roll out, instances scale up and down — services need to discover where their dependencies live right now.
- Client-side discovery: The caller queries a registry (Eureka, Consul) and decides which instance to call.
- Server-side discovery: A load balancer in front of the service queries the registry. The caller just talks to a stable name (this is what Kubernetes Services and AWS ELB do).
Configuration server
Centralized configuration so the same service binary runs in dev, staging, and prod — the difference is in the config it reads at startup. Spring Cloud Config, Consul KV, AWS Parameter Store, etcd. The point is to keep secrets and environment-specific values out of the image.
Message queue / event bus
Asynchronous communication for everything that does not need an immediate answer. RabbitMQ for reliable work queues. Kafka for high-throughput event streams and replay. SQS for simple AWS-native queueing. Async messaging is what lets the Order service confirm an order without waiting for the Notification service to send the email.
Circuit breaker and resilience
The pattern that keeps a slow downstream from cascading into a system-wide outage. Each service wraps its dependencies in a breaker that fails fast when the downstream is sick. This is the topic of its own tutorial — for now, know that without it, microservices fail in worse ways than monoliths, not better ones.
Observability stack
Three pillars: logs (what happened), metrics (how often / how fast), traces (the path a single request took across services). Without all three, the moment you have more than five services you cannot reason about what is happening in production.
Communication, Data, and Deployment Realities
Synchronous calls between services
The simplest case: one service makes an HTTP call to another and waits for the answer. Easy to write, easy to trace, easy to misuse — the moment you build a chain of three or four synchronous hops, your tail latency is the sum of all of them and one slow link slows the whole chain.
// Node.js — Order Service calls Payment Service
const express = require("express");
const axios = require("axios");
app.post("/orders", async (req, res) => {
const order = req.body;
try {
const payment = await axios.post(
"http://payment-service:5002/payments",
{ orderId: order.id, amount: order.total },
);
if (payment.data.status === "success") {
await saveOrder(order);
res.json({ status: "confirmed" });
}
} catch (err) {
res.status(500).json({ error: "payment failed" });
}
});
A more honest production-shaped service
# Python (FastAPI) — Order service that talks to two dependencies
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
import httpx
app = FastAPI(title="Order Service", version="1.0.0")
class OrderItem(BaseModel):
product_id: int
quantity: int
price: float
class Order(BaseModel):
order_id: Optional[int] = None
customer_id: int
items: List[OrderItem]
total_amount: float
status: str = "pending"
created_at: Optional[datetime] = None
async def verify_inventory(product_id: int, quantity: int) -> bool:
async with httpx.AsyncClient() as client:
r = await client.post(
"http://inventory-service:8001/verify",
json={"product_id": product_id, "quantity": quantity},
)
return r.json()["available"]
async def process_payment(amount: float, customer_id: int) -> bool:
async with httpx.AsyncClient() as client:
r = await client.post(
"http://payment-service:8002/charge",
json={"amount": amount, "customer_id": customer_id},
)
return r.status_code == 200
@app.post("/orders", response_model=Order, status_code=201)
async def create_order(order: Order):
for item in order.items:
if not await verify_inventory(item.product_id, item.quantity):
raise HTTPException(400, f"product {item.product_id} not available")
if not await process_payment(order.total_amount, order.customer_id):
raise HTTPException(402, "payment failed")
order.order_id = save_to_database(order)
order.status = "confirmed"
order.created_at = datetime.now()
return order
Asynchronous, fire-and-forget
// Spring Boot — User service notifies asynchronously
@Service
public class UserService {
@Autowired private UserRepository userRepository;
@Autowired private PasswordEncoder passwordEncoder;
@Autowired private RestTemplate restTemplate;
@Transactional
public User createUser(UserDTO dto) {
if (userRepository.existsByEmail(dto.getEmail())) {
throw new DuplicateEmailException("email exists");
}
User user = new User();
user.setEmail(dto.getEmail());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setCreatedAt(LocalDateTime.now());
User saved = userRepository.save(user);
// Non-blocking: the user is created even if notification fails
CompletableFuture.runAsync(() -> {
try {
restTemplate.postForEntity(
"http://notification-service:8003/notifications",
welcomeEmail(saved),
Void.class);
} catch (Exception e) {
logger.error("notification failed", e);
}
});
return saved;
}
}
Synchronous vs asynchronous — the rule of thumb
- Use sync when the caller cannot continue without the answer (charge a card, check inventory before confirming).
- Use async when the caller does not care about the answer right now (send the welcome email, update the search index, fan out an “order placed” event).
- If a chain of three sync calls would all be “the user does not need this right now,” the chain should be async.
Data ownership and the shared-DB anti-pattern
Each service owns the database that backs it. Other services do not query that database directly — they ask over the network. This is the rule that protects independence: if I can change my schema without telling you, then we are independent. If I have to send my migration to your team, we are not.
Anti-pattern: the shared schema
Sharing one database across services is the single fastest way to lose every benefit of microservices while keeping every cost. Schema migrations now require coordination across teams. One bad query starves everyone. The blast radius of a corrupt write is global. If two services need the same data, the right answer is for one to own it and the other to ask.
Containerized deployment
# Dockerfile for the Product service
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5001
CMD ["python", "product_service.py"]
# docker-compose.yml — bringing the mesh up locally
version: "3.8"
services:
product-service:
build: ./product-service
ports: ["5001:5001"]
environment:
DATABASE_URL: postgresql://db:5432/products
order-service:
build: ./order-service
ports: ["5002:5002"]
environment:
DATABASE_URL: postgresql://db:5432/orders
PRODUCT_SERVICE_URL: http://product-service:5001
user-service:
build: ./user-service
ports: ["5003:5003"]
environment:
DATABASE_URL: postgresql://db:5432/users
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: secret
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Scaling Considerations
The promise of microservices is “scale only the part that needs it.” The reality is more nuanced — you can do this, but you also have to scale the things around the services.
Per-service scaling: where the win is
The Search service in an e-commerce site might handle 50x the traffic of the Returns service. In a monolith, both scale together — you pay for Returns capacity to handle Search load. In microservices, each scales on its own metrics: Search scales on QPS, Returns scales on queue depth, image processing scales on CPU.
| Service | Typical scaling signal | Typical shape |
|---|---|---|
| API gateway / catalog | Requests per second | Many small stateless replicas behind a load balancer |
| Image / video processing | CPU + queue depth | A pool of workers consuming a job queue |
| Recommendations / search | P99 latency, cache hit rate | Memory-heavy boxes, often with local indexes |
| Payments / orders | Modest QPS, but transactional | Fewer replicas, careful with DB connections |
| Notifications | Queue depth | Async workers; horizontal-only |
Granularity: too small, just right, too large
The right size for a service is the size where one team can own it end to end without becoming a bottleneck and without becoming bored. Too small and you turn local function calls into network hops for no reason. Too large and you are back to a monolith with extra YAML.
| Too small | Just right | Too large |
|---|---|---|
| Excessive network calls for one user action | Clear single purpose, named in one phrase | Multiple unrelated responsibilities |
| Hard to follow what happens during a request | One team can own and operate it | Multiple teams need to coordinate to ship |
| Deploy and observability overhead per change | Independently deployable on its own schedule | Cannot deploy a part without redeploying the whole |
| Latency budget eaten by hops | Latency budget realistic for its job | Internal coupling that should have been a network seam |
Conway’s Law: the architecture follows the org chart
“Organizations design systems that mirror their communication structures.” — Melvin Conway
This is not a slogan. It is an observation. If two teams have to talk constantly to ship anything, they will end up owning one service together — or two services that are coupled enough that they may as well be one. The corollary: pick service boundaries that follow your team boundaries, or change one until they match.
Bounded contexts: the same word means different things
From Domain-Driven Design: a bounded context is the slice of the business where one model holds. The word “Customer” means different things in different contexts — and that is fine. Each service models the customer in the way that fits its job:
# Sales context — Customer as buyer
class Customer:
customer_id: str
credit_limit: Decimal
purchase_history: List[Order]
loyalty_points: int
# Support context — Customer as case
class Customer:
customer_id: str
support_tier: str # bronze, silver, gold
open_tickets: List[Ticket]
satisfaction_score: float
# Shipping context — Customer as recipient
class Customer:
customer_id: str
shipping_addresses: List[Address]
delivery_preferences: dict
One id, three models. Each owned by the team that uses it. None reaches into the other.
Real-World Examples
The big consumer companies are the ones whose engineering posts everyone has read. Their architectures are interesting not because they are typical — they are not — but because they show what microservices look like at the scale where the trade actually pays off.
Amazon: the two-pizza team
Amazon famously structures around the “two-pizza team” rule: if a team needs more than two pizzas to feed, it is too large. Each team owns a service end to end — build, deploy, operate, on-call. This is the org-chart side of microservices made explicit.
- Team size: 5–10 people per service.
- Ownership: the full lifecycle — build, ship, run, page.
- Autonomy: tech and architecture decisions live with the team.
- Outcome: thousands of independent services with minimal cross-team coordination, and the highest deploy frequency in the industry.
Netflix: from monolith to hundreds of services
Netflix evolved from a DVD-shipping monolith into a cloud-native streaming platform with hundreds of microservices. They open-sourced Eureka (service discovery), Hystrix (circuit breakers), Zuul (API gateway), and the Simian Army (chaos engineering). The pattern of “everything fails, design for it” is largely Netflix’s contribution to industry practice.
- Recommendation Service: picks what to suggest based on viewing history.
- Streaming Service: serves the actual video bytes.
- Billing Service: subscriptions, payments, dunning.
- User Profile Service: accounts, preferences, parental controls.
- Result: deploys thousands of times per day, serves over 200M users worldwide, and a bad recommendation never stops the playback.
Uber: 2,200 services and dispatch in under a second
Uber operates one of the largest microservice deployments in the world to handle 100M+ trips per day across 10,000+ cities.
| Service | Purpose | Scale signal |
|---|---|---|
| Dispatch Service | Match riders with drivers | Real-time geospatial calculations |
| Surge Pricing | Dynamic pricing on demand | Updates every few seconds per region |
| Maps Service | Routing and navigation | Billions of map tiles |
| Payment Service | Charge and pay out | Multi-currency, multi-region |
| Driver Service | Manage driver availability | Real-time location tracking |
To make this work, Uber built and open-sourced a stack of supporting tools: TChannel (RPC), Ringpop (distributed hash ring for routing), Cadence (workflow orchestration), Jaeger (distributed tracing), and M3 (metrics handling 500M+ data points per second). The same codebase serves Uber, Uber Eats, and Uber Freight.
The honest takeaway: this scale of microservices requires this scale of investment in tooling. Most companies should not try to copy Uber’s architecture — they should copy Uber’s rigour about when to add a new service.
Spotify: squads, tribes, and service ownership
Spotify made the org-chart side of microservices explicit with the “squad” model: small cross-functional teams, each owning a slice of the product end to end. Squads are grouped into “tribes” aligned with a major capability. The architecture follows the org — each squad ships and runs its own services on its own cadence.
The pattern across all four
- None of them started as microservices. All started as monoliths and migrated when growth made it the cheaper path.
- All four invested heavily in shared infrastructure — service discovery, deploy systems, observability — before pushing service count up.
- All four organized teams around services, not the other way around. The service map mirrors the org chart on purpose.
- All four accept some operational complexity that smaller teams could not afford. They do it because the win — ship velocity at scale — is bigger than the cost.
Best Practices
The short list
- Start with a modular monolith. Get the boundaries right in code first — modules with clear interfaces — before you make them network seams. It is much cheaper to refactor module boundaries than service boundaries.
- One team per service, one service per team. If two teams own one service or one team owns five services, the org and the architecture are fighting each other.
- Each service owns its data. No shared schemas, no cross-service joins. If two services need the same data, one owns it and the other asks.
- Services talk only over the wire. No shared libraries that hide inter-service coupling. The contract is the API; the implementation is private.
- Build the platform before the second service. CI/CD, container registry, service discovery, observability, secret store, on-call rotation. The first service feels like overkill. The tenth service feels like home.
- Async by default for non-critical paths. Welcome emails, search index updates, audit log writes — none of these should block the user’s response.
- Pair every breaker with a fallback, every retry with a budget, every timeout with a metric. Resilience patterns are part of every service, not an afterthought.
- Treat operations as a first-class deliverable. Runbooks, dashboards, alerts, and on-call rotations are part of “done.”
The migration path, if you are not there yet
If your honest answer to most of the readiness questions is “not yet” but you believe microservices are in your future, do this in order:
- Build a modular monolith. Strict module boundaries, internal APIs between modules, separate schemas inside one database. This is the single most useful first step and most teams underrate it.
- Build the DevOps foundation. CI/CD, infrastructure as code, structured logging, metrics, alerting, on-call. None of this requires microservices, all of it makes microservices possible.
- Grow the team. Microservices solve a coordination problem you do not have until you have multiple teams.
- Extract the first service. Pick a bounded context that is on the edge — not the heart of the business. Notification, search, image processing. Not checkout.
- Run the monolith and the service side by side. Use the Strangler Fig pattern: route a percentage of traffic to the new service, watch every metric, increase the percentage as confidence grows.
- Iterate. Each new service is cheaper than the last because the platform you built for the first one is now reused. After three or four extractions you have a real microservices practice.
The Strangler Fig in practice
# Routing layer — gradually move /users from monolith to new service
from flask import Flask, request, redirect
import requests
app = Flask(__name__)
@app.route("/users/<path:path>", methods=["GET", "POST", "PUT", "DELETE"])
def user_proxy(path):
if is_feature_enabled("new_user_service"):
r = requests.request(
method=request.method,
url=f"http://user-microservice:5000/{path}",
headers=request.headers,
data=request.get_data(),
)
return (r.content, r.status_code)
return redirect(f"http://monolith:8000/users/{path}")
The fig grows around the tree. The tree dies. The fig is left standing. That is the migration in one sentence.
The single most useful sentence about microservices
Microservices are an organizational pattern that happens to be implemented in software. The hardest problems are not technical — they are about teams, ownership, and decision-making. If your teams cannot ship independently today as a monolith, splitting the codebase will not make them able to. Fix the org first; the architecture will follow.