Microservices Foundations
Before diving into implementation, it’s crucial to understand when and why to use microservices. Many teams adopt microservices for the wrong reasons and end up with a distributed monolith that’s harder to maintain than what they started with. Think of it like home renovation. A monolith is a studio apartment — everything is in one room, which is perfectly fine when you live alone. Microservices are like converting that studio into a multi-room house. You get privacy, dedicated spaces, and the ability to renovate one room without tearing down the whole building. But you also need hallways (networks), doors (APIs), a shared electrical system (infrastructure), and a much bigger budget. If you are a solo occupant, the studio was always the right call. The multi-room house only makes sense when you have a family (multiple teams) with genuinely different needs.- Understand the evolution from monolith to microservices
- Learn when to choose microservices vs monolith
- Master key microservices principles
- Identify common anti-patterns to avoid
What Are Microservices?
Microservices architecture is a software design approach where an application is built as a collection of small, independent services that:- Run in their own process - Each service is deployed independently
- Communicate via lightweight protocols - Usually HTTP/REST or messaging
- Are organized around business capabilities - Not technical layers
- Can be deployed independently - Without affecting other services
- May use different technologies - Best tool for each job
Monolith vs Microservices
The Monolith
A monolithic application is a single deployable unit containing all business functionality.- Advantages
- Disadvantages
- Single codebase, easier to understand
- Simple deployment (one artifact)
- Easy local development
- Straightforward debugging
- ACID transactions across all data
- No network latency between modules
- Shared memory, faster communication
- No serialization/deserialization overhead
- Easier to refactor
- Simple IDE navigation
- Consistent tech stack
- Unified testing
Understanding the Monolith in Code
Before we look at code, it’s worth pausing on why monoliths dominate early in a product’s life. The honest answer is that they are dramatically simpler along almost every axis that matters when you have a small team: one git repo, one deployment pipeline, one runtime, one database connection pool, one set of logs. When something goes wrong at 2am, you open one debugger, attach to one process, and follow one stack trace. If you were to skip this and start with microservices instead, you would spend your first three months building infrastructure (service discovery, distributed tracing, event buses) rather than building the features that prove your product idea is worth pursuing. In the broader microservices architecture, the monolith is your starting point and often your fallback — you extract services from a monolith as organizational pressure mounts, not the other way around. The tradeoff to watch: the monolith will eventually become painful if your team grows past ~15 engineers or if you have wildly different scaling needs between features. Recognizing that moment is an art, not a science.- Node.js
- Python
Microservices
- Advantages
- Disadvantages
- Deploy services independently
- Faster release cycles
- Lower risk deployments
- Zero-downtime updates
- Scale services independently
- Optimize resources per service
- Handle varying loads efficiently
- Clear ownership
- Independent technology choices
- Parallel development
- Easier onboarding (smaller scope)
- Fault isolation
- Graceful degradation
- Independent failure recovery
The Scalability Fallacy: Microservices Are Not A Scaling Strategy
One of the most persistent myths in our industry is that microservices are “how you scale.” They are not. Microservices are how you scale teams, not traffic. A well-tuned monolith on a beefy machine routinely handles more requests per second than a tangle of 15 poorly-designed microservices with network hops between every step.Team Cognitive Load: The Hidden Scalability Ceiling
The real reason microservices scale is not traffic — it is cognitive load on teams. Below 10-15 engineers, one team can hold the whole monolith in their collective head. Above 30-40 engineers, nobody can, and the monolith becomes a graveyard of code that everyone is afraid to touch.Interview: Your CTO says 'Our Rails monolith can't scale past 50K RPS. We need to migrate to microservices immediately.' How do you respond?
Interview: Your CTO says 'Our Rails monolith can't scale past 50K RPS. We need to migrate to microservices immediately.' How do you respond?
- Challenge the premise with data. Ask for the current P50/P99 latency numbers, the bottleneck endpoint, and the CPU/DB utilization at peak. If they do not have these, scaling is a guess.
- Walk through the vertical-scaling options first. Bigger instance, read replicas, query optimization, caching. Most monoliths can reach 500K+ RPS with proper tuning; Shopify ran on Rails at that scale.
- Quantify the microservices cost. Roughly 30-40% engineering capacity for 12-18 months, plus ongoing operational tax. Present this as a real budget line.
- Offer a diagnostic spike. Two weeks to profile the monolith and identify what is actually slow. Publish the findings.
- If extraction is truly needed, extract one bottleneck component. Strangler Fig, measure, learn, then decide on the next one.
- “How do you explain the ‘distributed systems tax’ concretely to a non-technical CTO?” Use a concrete analogy: “Today, calling getUserById is a function call — 10 nanoseconds. After microservices, it is a network call — 5 milliseconds. We just made every user lookup 500,000x slower. For that to be worth it, we need to be saving engineer-months, not CPU cycles.”
- “What if the CTO insists?” Agree on metrics that would prove the migration is working and metrics that would prove it is failing. Commit to re-evaluating in 90 days. Documented kill criteria are the most powerful tool against resume-driven architecture.
- “What if profiling shows the monolith really is the bottleneck?” Usually the answer is a specific hot path — search, video transcoding, real-time chat. Extract that one thing as a service with its own scaling profile. Keep the rest in the monolith.
- “Yes, microservices will solve the scaling issue.” Fails because it accepts the false premise that microservices are a scaling pattern. Microservices add network latency; they do not remove work.
- “We should just move to Kubernetes.” Fails because it conflates orchestration with architecture. K8s runs monoliths just fine and does not by itself improve latency.
- Shopify Engineering, “Surviving Black Friday at Monolith Scale” (2019).
- Sam Newman, “Building Microservices” 2nd ed., Chapter 4 (on coupling and scaling).
- DHH, “The Majestic Monolith” and “The Modular Monolith” posts on 37signals.
Interview: How would you explain the cost of microservices to a skeptical CTO who just read 'Monolith to Microservices' and wants to migrate?
Interview: How would you explain the cost of microservices to a skeptical CTO who just read 'Monolith to Microservices' and wants to migrate?
- Frame the conversation around total cost of ownership, not migration cost. Migration is a one-time hit; operational overhead is forever.
- Break the cost into five explicit buckets (observability, data consistency, testing, network reliability, operational toil) and estimate each.
- Present the “steady-state tax.” In the first year, expect 30-40% of engineering capacity on infrastructure. Years 2+, expect 15-20% ongoing.
- Identify the break-even point. At what team size or deploy frequency do these costs pay back? Usually 30-50 engineers and 10+ deploys per day.
- Propose the modular monolith as the first investment. Strict module boundaries + separate schemas + architectural tests gets you 70% of the value for ~5% of the cost.
- “What’s the single most-underestimated cost?” Distributed debugging. In a monolith, a bug is a stack trace. In microservices, a bug is “something happened somewhere across 12 services in the last 200ms — find it.” Distributed tracing helps, but engineers still take 3-5x longer to diagnose cross-service issues.
- “How do you budget for the transition period?” Assume the migrated services will have more bugs for 6-12 months, because the team is learning new patterns (idempotency, sagas, circuit breakers). Add a 20% feature velocity drop to your forecast.
- “What’s the one investment that pays for itself fastest?” Distributed tracing (Jaeger, Tempo, Datadog APM). Without it, every cross-service incident becomes a multi-hour investigation. Set it up before extracting service #2.
- “The cost is mostly just the extra servers.” Fails because infrastructure cost is the smallest line item. Engineering time (both migration and ongoing operational) dominates.
- “We can avoid most costs by using managed services.” Fails because managed services reduce operational cost but not the fundamental complexity cost. Your team still needs to understand sagas, idempotency, and eventual consistency.
- Sam Newman, “Monolith to Microservices” (O’Reilly, 2019), Chapters 1-3.
- Etsy Engineering, “Continuous Deployment at Etsy” (2014) — how they achieved team independence within a monolith.
- Google SRE book, Chapter 21 (Handling Overload) — quantifies the cascading-failure cost in distributed systems.
Interview: You're consulting for a team hitting 'partial failures' in production -- one service sometimes returns stale data. How do you explain eventual consistency to their confused PM?
Interview: You're consulting for a team hitting 'partial failures' in production -- one service sometimes returns stale data. How do you explain eventual consistency to their confused PM?
- Define the terms without jargon. “Strong consistency” means everyone sees the latest value immediately. “Eventual consistency” means everyone will see it — eventually, usually within seconds. You trade immediacy for availability.
- Map it to a business concept the PM already understands. Email delivery. You send an email; it arrives “eventually.” Nobody expects instant delivery. That is eventual consistency as a business norm.
- Explain why microservices force this choice. Each service has its own database. Writes to service A cannot atomically update service B’s DB without distributed transactions (which have their own severe costs — 2PC, performance, failure modes).
- Show the concrete symptoms the PM should expect. “After you click Save, it may take up to 2 seconds for search results to update.” That is a product spec, not a bug.
- Offer the mitigation. UI patterns (optimistic updates, “saving…” indicators), idempotency keys, and event-driven reconciliation.
- “How do you handle the rare case where the user sees truly inconsistent data?” Idempotency + reconciliation. The backend reconciles within seconds; the UI shows a merge banner if the user’s view drifts. Google Docs does this with every concurrent edit.
- “What if the PM says ‘just make it strongly consistent’?” Show them the CAP theorem in plain English: in a distributed system, during a network partition, you must choose between consistency and availability. Most consumer products choose availability, because “briefly wrong” beats “briefly down.”
- “When is eventual consistency not acceptable?” Financial correctness. Do not let the checkout charge the user 90. Use sagas with compensations and verify the total synchronously at checkout time.
- “Use distributed transactions to keep everything consistent.” Fails because 2PC across services is a known anti-pattern — it creates single points of failure and dramatically increases latency.
- “It’s just a caching bug — invalidate the cache.” Fails because eventual consistency is structural, not a cache artifact. You cannot invalidate your way out of it.
- Werner Vogels, “Eventually Consistent” (ACM Queue, 2008).
- Martin Kleppmann, “Designing Data-Intensive Applications” (O’Reilly, 2017), Chapter 9.
- Pat Helland, “Life Beyond Distributed Transactions: An Apostate’s Opinion” (CIDR, 2007) — the foundational paper.
The Honest Trade-off Analysis
The microservices vs. monolith decision is not a technology choice — it is an organizational choice. You are trading local complexity (one big codebase) for distributed complexity (many small codebases connected by a network). Neither is inherently better.When to Use Microservices
Use Microservices When:
Large Team
Different Scaling Needs
Technology Diversity
High Availability
Clear Domain Boundaries
Frequent Deployments
Don’t Use Microservices When:
Small Team
Startup MVP
Simple CRUD
Limited DevOps
Unclear Boundaries
Strong Consistency Needed
The Microservices Decision Matrix
Use this matrix to evaluate if microservices are right for your situation:- Under 35: Stick with monolith
- 35-50: Consider modular monolith
- > 50: Microservices may be beneficial
The Distributed Systems Tax
Every microservices architecture pays a “distributed systems tax” — a set of costs that do not exist in a monolith. Understanding this tax deeply is what separates a senior engineer from a mid-level one in system design interviews.Key Microservices Principles
1. Single Responsibility
Each service should do one thing well and have a clear, bounded responsibility.2. Loose Coupling
Services should minimize dependencies on other services. Think of loose coupling like departments in a company communicating through formal memos rather than by rummaging through each other’s filing cabinets. Each department controls access to its own records and exposes only what others need through defined channels.Why This Matters Before Seeing Code
Loose coupling is the single most important property of a healthy microservices system. The moment two services share a database table, a Redis key, or even an in-memory cache, you have re-invented the monolith’s coupling with none of its benefits — and now you cannot even use database-level foreign keys to protect yourself. A tightly-coupled pair of services must be deployed together, tested together, and often debugged together. They share an implicit schema that no compiler enforces, so breakages happen silently at runtime. If you ignore this principle and let services reach directly into each other’s storage, your architecture looks distributed on paper but behaves as a monolith in practice. Any change to one schema cascades through the entire system, and independent deployment becomes impossible. The tradeoff to watch: loose coupling via API calls introduces network failures, latency, and serialization overhead. You are buying maintainability at the cost of runtime complexity. This is almost always a good trade, but you must design for the failure modes (circuit breakers, timeouts, retries) from day one.- Node.js
- Python
3. High Cohesion
Related functionality should be grouped together within a service. Cohesion is the positive mirror of coupling: where loose coupling minimizes what crosses service boundaries, high cohesion maximizes what belongs together within a single boundary. A cohesive service is one you can describe in a single sentence without using the word “and.” “Manages user identity and authentication” is cohesive. “Manages users and sends marketing emails” is not — those are two jobs pretending to be one. If you get cohesion wrong and split related logic across services, you will pay the “distributed transaction” tax on every feature. Adding a new user preference field would require changes in five services coordinated via a release train. In the broader architecture, high cohesion is what allows a team to move fast: they touch one service, one database, one test suite, one deploy pipeline. The tradeoff: a highly cohesive service can grow large. That is fine — “small” is a consequence of cohesion and coupling being correct, not a goal unto itself.- Node.js
- Python
4. Database Per Service
Each service should own its data and expose it only through APIs. This is polyglot persistence — each service picks the storage engine best suited to its access patterns, rather than every module sharing one Postgres instance and fighting over schema migrations. The trade-off is explicit: you gain deployment independence and technology freedom, but you lose cross-service joins and ACID transactions. In a monolith, “give me all orders for users who signed up last week” is a single SQL join. In microservices, it becomes two API calls and client-side joining. That cost is real, and it is worth paying only when the benefits of independent deployment outweigh it.Why This Is the Hardest Principle to Accept
This principle is where most teams hesitate, and for good reason — it feels wrong. Your DBA has spent a decade optimizing one Postgres cluster with foreign keys, referential integrity, and beautiful query plans. Now you are being told to throw that away and accept eventual consistency across a dozen heterogeneous stores. Yes. That is exactly what you are being told, and there is no way around it if you want true service independence. If you violate this principle and share a database, you get “independent deployment” in name only. Schema migrations become globally coordinated events. One team’s index change degrades another team’s queries. Worst of all, you lose your ability to evolve services independently, because the database schema is the most coupling thing in the entire stack. Within the broader architecture, the database-per-service rule is what actually unlocks technology diversity, scaling per-service, and bounded-context ownership. The tradeoff to watch: you will need to rebuild cross-service queries using event-driven read models (CQRS, materialized views), API composition, or dedicated reporting databases fed by change data capture.- Node.js
- Python
5. Design for Failure
Assume other services will fail and design accordingly. In a monolith, a function call either returns or throws — there is no ambiguity. In microservices, a network call can succeed, fail, succeed slowly, succeed but return stale data, or hang forever. You are designing for eight failure modes instead of two, and every one of them needs a plan.The Mental Model Shift Before You Write Code
The biggest mental adjustment for engineers new to microservices is accepting that every network call is a tiny distributed system with its own partial failure modes. The naive approach is to wrap every call in try/catch and call it a day. That does not work, because the most dangerous failure mode is not an error — it is a slow response. A 30-second timeout on a call that is made 1,000 times per second will exhaust your thread pool and take down your service, even though technically nothing “failed.” If you skip resilience patterns and assume the network is reliable, you will experience the classic cascade failure: User Service gets slow, Order Service blocks waiting for it, threads pile up, memory fills with pending requests, and now Order Service crashes too. Then Payment Service, which was calling Order Service, fails. Then everything fails. Circuit breakers, timeouts, and fallbacks exist specifically to contain this cascade. In the broader architecture, resilience patterns are what make independent failure recovery actually work — without them, “fault isolation” is just a diagram on a slide. Tradeoff: fallbacks add code complexity and can mask real problems if not instrumented carefully.- Node.js
- Python
Anti-Patterns to Avoid
1. Distributed Monolith
Services are deployed separately but still tightly coupled.2. Nano-Services
Services are too small, creating unnecessary complexity.3. Wrong Service Boundaries
Services split by technical layers instead of business domains.4. Shared Libraries Nightmare
Too much shared code creates hidden coupling. Think of shared libraries like a shared lease on an apartment — every change requires all tenants to agree, and one tenant’s renovation can break another’s furniture arrangement.- Node.js
- Python
The Strangler Fig Pattern
The safest way to migrate from monolith to microservices.Project: Service Decomposition Exercise
Decompose this e-commerce monolith into microservices:Current Monolith Features:
- User registration and authentication
- Product catalog management
- Shopping cart
- Order placement and tracking
- Payment processing
- Inventory management
- Reviews and ratings
- Notifications (email, SMS, push)
- Search functionality
- Recommendations
Your Task:
- Identify Services: List the microservices you would create
- Define Boundaries: What does each service own?
- Data Ownership: Which database for each service?
- Communication: How do services communicate?
- Dependencies: Draw the dependency graph
Solution
Solution
Interview Questions
Q1: When would you NOT use microservices?
Q1: When would you NOT use microservices?
- Small team (under 10 developers)
- Startup MVP still validating product-market fit
- Simple CRUD applications
- Limited DevOps capabilities
- Unclear domain boundaries
- Strong consistency requirements across domains
- When network latency would significantly impact user experience
Q2: How do you determine service boundaries?
Q2: How do you determine service boundaries?
- Bounded Contexts: Identify areas where models/language differ
- Business Capabilities: Align with business functions
- Data Ownership: Who owns the data?
- Team Structure: Conway’s Law considerations
- Change Frequency: What changes together?
- Splitting by technical layers (UI, DB, API)
- Creating nano-services
- Sharing databases between services
Q3: What is the strangler fig pattern?
Q3: What is the strangler fig pattern?
- Identify a bounded context to extract
- Build the new microservice alongside the monolith
- Route traffic to new service (using API gateway/proxy)
- Migrate data
- Decommission old code
- Repeat for next context
Q4: What is a distributed monolith?
Q4: What is a distributed monolith?
- Tight coupling between services
- Shared databases
- Synchronous call chains
- Must deploy multiple services together
- Single point of failure affects everything
Summary
Key Takeaways
- Microservices are not always the answer
- Start with a monolith, extract when needed
- Define clear service boundaries
- Each service owns its data
- Design for failure from the start
Next Steps
Interview Deep-Dive
'Walk me through how you would determine the right size for a microservice. What is too small? What is too big?'
'Walk me through how you would determine the right size for a microservice. What is too small? What is too big?'
'Your team is building an MVP for a new product. The CTO wants microservices from day one. Do you push back?'
'Your team is building an MVP for a new product. The CTO wants microservices from day one. Do you push back?'
The Scalability Story: How Scale Breaks Monoliths
The monolith-to-microservices journey is not something you decide on a whiteboard. It happens to you. You start out with a perfectly reasonable monolith because you have a team of four, a product that might not exist in a year, and no operational budget. Then you succeed. And as you succeed, the monolith that was perfect for four people starts to groan under the weight of your growth. Here is what that actually feels like, stage by stage.Stage 1: 1,000 Users — Everything Is Fine
You have one application, one database, maybe a Redis instance for sessions. Your CI pipeline takes 8 minutes. Deploys are a git push and a kubectl rollout. When something breaks, you open one log stream and find the stack trace in seconds. Four engineers share the codebase and meet in person to resolve conflicts. The monolith was absolutely the right call and remains so. Nobody has any reason to change anything.Stage 2: 100,000 Users — Vertical Scaling
Traffic has grown 100x. The checkout flow — which hits the payment gateway, writes to 3 tables, and sends a confirmation email — is slow under load. Your first instinct is correct: resize the box. You go from a 4-core VM to a 32-core VM. The database goes from 8GB of RAM to 64GB. Everything is fast again. Total operational cost: $2,000/month. Total engineering time spent: one afternoon. This is vertical scaling, and for most applications it is genuinely the best answer until you hit somewhere between 500K and 5M users, depending on workload. The problem with vertical scaling is not that it does not work — it works great — it is that it stops working eventually, and when it stops working there are only a few knobs left to turn. AWS’s biggest EC2 instance caps out. Your Postgres single-writer architecture caps out. You cannot double performance by buying more expensive hardware forever.Stage 3: 1,000,000 Users — The First Cracks
You start seeing specialized scaling needs. Your product catalogue gets 10,000 reads per second; every page view hits it. Your checkout gets 50 writes per second. These have wildly different profiles. You add a Redis cache in front of the catalogue. Great — catalogue reads now hit Redis 95% of the time and fly. But the cache is bolted onto the monolith, which means every module now has access to it, whether they should or not. A well-meaning engineer uses the cache for session data and another for rate limits. The cache’s blast radius is now the entire app. Meanwhile, checkout is still the bottleneck. It is CPU-bound, doing payment cryptography and tax calculations. The rest of the app would be happy on 8 cores, but checkout wants 32. You provision the whole fleet for checkout’s needs and pay 4x the compute cost on every pod. You cannot scale checkout independently because it lives in the same binary as catalogue, user profiles, recommendations, and 40 other modules.Stage 4: 10,000,000 Users and 40 Engineers — Team Coupling
This is the stage where the organizational problems eclipse the technical ones. You now have eight teams of five engineers each. Every team wants to ship features. But there is only one codebase, one test suite, one deploy pipeline. Here is what a Tuesday looks like: the recommendations team pushes a change that introduces a subtle memory leak. It does not manifest until four hours after deploy, under load. By then, the checkout team has merged their work, the notifications team has merged theirs, and five other teams are queued up. At 10:47 PM, checkout errors start climbing. Nobody knows whose change caused it because the last deploy contained 14 merges. The on-call engineer rolls back the whole deploy, which reverts everyone’s work. Tomorrow morning, seven teams have to rebase and retest. Velocity has dropped to near-zero. One bad line in recommendations broke checkout. This is deploy coupling — unrelated work is strapped together by the single deploy artifact.The Inflection Point
There is no magic number of users or engineers at which microservices become correct. What matters is the ratio of coordination cost to build cost. When your engineers spend more time resolving merge conflicts, waiting for CI, untangling shared-state bugs, and coordinating deploys than they spend writing the features those activities support, you have hit the inflection point. In my experience this tends to happen around 15-25 engineers for a typical web app, or earlier for a product with dramatically different scaling profiles per feature (real-time chat + video encoding + catalogue browse in one codebase is asking for pain by engineer #12). The mistake is either reaching for microservices too early (stage 2 or 3, when vertical scaling is still winning) or too late (stage 5, when the coordination tax has already destroyed velocity for a year). The right move is usually: start extracting services at the precise team-coupling boundaries that hurt the most, one at a time, using the Strangler Fig pattern. Do not do a big-bang rewrite. Do not extract services that are not yet causing pain. Pain is the signal.Intent Parity: The Human Contract
Every API call your system receives represents a human intent. “I want to buy this.” “I want to send this message.” “I want to update my profile.” “I want to cancel my subscription.” Behind every HTTP request is a human being with a goal, and that human does not care about your thread pools, your distributed transactions, or your circuit breakers. They care about one thing: did the system do what they asked? Microservices are reliable precisely to the degree that they honor these intents even when the underlying execution is degraded. Intent parity is the architectural commitment that a user’s original request — their intent, captured at the moment of the click or API call — will be eventually fulfilled, not silently dropped. The outcome may be delayed. The intent is never lost.Event Sourcing as Intent Preservation
Event sourcing stores the complete sequence of state-change events — every intent the system has recorded — as the source of truth. Instead of storing “cart currently contains: [item A, item B]”, you store the events: “user added item A at 10:01”, “user added item B at 10:03”, “user removed item A at 10:05”. The current state is derived by replaying those events. The reason this matters for intent parity: every intent is a first-class, immutable record. If your downstream materialized view (the query model) gets corrupted or lost, you replay from the events and the system self-heals. The intent is never lost, because the intent is the storage. Traditional CRUD loses the “why” and the “when” the moment you overwrite state. Event sourcing keeps every intent forever.The Outbox Pattern as Durable Intent Capture
The outbox pattern is the simplest, most production-ready way to guarantee intent parity for services that cannot yet adopt full event sourcing. The pattern: when your service handles a request, it writes two things in the same local database transaction — the business state change AND an “outbox” row describing the event that should be sent to downstream systems. A separate process reads new outbox rows and publishes them to Kafka, SQS, or wherever. Why this guarantees intent parity: both writes commit atomically (they are in the same transaction). If the database commits, the intent is durably recorded. If the subsequent Kafka publish fails, the outbox row is still there — the publisher retries forever until it succeeds. If the service crashes mid-publish, the next instance picks up the outbox and continues. The intent cannot be lost short of catastrophic data corruption. Contrast this with the “naive” approach: write to the database, then publish to Kafka. If the database commit succeeds but the Kafka publish fails (network blip), you have a state change with no event, and downstream systems never find out. The user’s intent was recorded but never fulfilled end-to-end. That gap is exactly what intent parity demands you close.Sagas as Distributed Intent Fulfillment
When one user intent requires changes across multiple services — “place this order” touches Orders, Inventory, Payment, and Notifications — you need a mechanism to drive all of those changes to completion or to a consistent compensation state. That is a saga. A saga is a sequence of local transactions, each in a different service, coordinated by events (choreography) or a central orchestrator. If step 3 of 5 fails, the saga runs compensating actions for steps 1 and 2 to leave the system in a consistent state. The user’s intent is either fully honored (happy path) or cleanly reverted (compensation path) — it is never left in a half-complete, ambiguous state. This is intent parity for distributed workflows. The saga is the machinery that turns “user clicked Place Order” into either “order placed successfully, inventory reserved, payment captured, notification sent” or “order cancelled, no inventory held, no charge, apology email sent.” There is no middle ground where the user is charged but the order was not created. That middle ground is what intent parity refuses to accept.Maintainability: Conway’s Law
In 1967, Melvin Conway published a short paper containing an observation that has haunted every software architecture decision ever since:“Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.”This is Conway’s Law, and it is not advice — it is a description of what will happen whether you plan for it or not. If you have three teams, you will end up with roughly three major components. If your backend and frontend teams never talk to each other, you will end up with a backend API that does not fit what the frontend actually needs. If your payments team is organizationally separate from your orders team, the seam between those two systems will calcify into an API boundary. You cannot fight this. Engineers have tried, and they lose. What you can do is use Conway’s Law deliberately: design your org chart to match the architecture you want. This is sometimes called the inverse Conway maneuver, and it is one of the highest-leverage moves in organizational design.
The Story: Why Spotify’s Squads Work
Spotify’s famous “squad” model is often misunderstood as “agile, but trendier.” It is actually a direct application of inverse Conway. Each squad owns a specific user-facing capability (search, recommendations, library management) end-to-end: frontend, backend, data pipeline, operational concerns, everything. Because one team owns the whole vertical, the service boundary naturally forms at the edge of that team’s concern. Squads do not step on each other’s code because they do not share code in ways that matter to daily work. The magic is not the word “squad.” It is the alignment between team ownership and service boundary. If you replaced “squad” with “team” and dropped all the Spotify marketing, the underlying principle — one team owns one service end-to-end — would still work. The reason Spotify’s architecture held up through hypergrowth was that the team structure and the service structure were the same shape.What Goes Wrong When You Ignore Conway
The mirror image: companies that try to impose a microservices architecture on a traditional functional org (separate frontend team, backend team, database team, QA team). What happens is predictable and sad. Every feature requires coordination across four teams. Every service ends up with four different owners, none of whom can unilaterally deploy it. Services get architected to minimize cross-team coordination, which means they absorb responsibilities they should not have just to avoid calling another team’s API. Within a year, you have a monolith held together with HTTP calls — a distributed monolith — because the org chart made real service boundaries impossible. If you want independent services, you need independent teams. If you only have functional silos, you will get a system with functional-silo boundaries, no matter how many diagrams you draw.Using Conway Deliberately
Here is the practical play. When you start planning a microservices migration or a significant architecture shift, redraw your org chart first.- List the services you want. For each service, identify the team that owns it end-to-end. If no such team exists, you need to form one before the service exists, or the service will degenerate.
- Minimize cross-team dependencies. If two teams constantly need to coordinate to ship a feature, their services are probably in the wrong places, or the two teams should merge.
- Give each team its own deploy pipeline and on-call rotation. This is the real test of service ownership: can this team deploy at 3 AM without waking anyone else up?
- Accept some redundancy. Inverse Conway often means two teams build similar-looking internal tools, because neither is willing to depend on the other. That is fine. The coordination cost of sharing is often higher than the cost of a little duplication.