SLIDE 66: Section Intro
Section 5: Data Ownership and Read Models
This is where microservices get genuinely hard. Most engineers can split a system into services and pick communication protocols. Far fewer can answer the question that follows: when each service owns its own database, how do you handle the data that multiple services need?
In a monolith, you join tables. In microservices, those tables live in different databases owned by different services. You cannot join across service boundaries. Every pattern in this section exists to solve that fundamental problem.
What We Will Cover:
- The database-per-service principle and why it is non-negotiable
- The shared database anti-pattern and exactly how it breaks independence
- Cross-service reads: API composition and materialized views
- Cross-service writes: events and workflows
- Data duplication as a deliberate trade-off
- Eventual consistency and the read-your-writes problem
- CQRS: separating write models from read models
- Reference data ownership, search indexing, cache ownership, and reporting
- Schema migration strategies across independently deployed services
Where This Appears in Real Systems:
In a platform like Amazon, when you view a product page, the data comes from at least five different services: catalog (product details), inventory (stock status), pricing (current price), reviews (ratings), and recommendations (similar products). None of these services share a database. The product page is assembled from data owned by independent services, each using a different strategy to make its data available.
SLIDE 67: Database per Service
Every microservice must own its own database. This is the most important data rule in microservices and the one that creates the most practical challenges.

What “owns its own database” means:
- The Order Service is the only service that can read from or write to the Order database
- No other service has credentials to connect to the Order database
- No other service knows the Order database schema (table names, column types, indexes)
- If the Payment Service needs order data, it calls the Order Service API. It does not run SQL against the Order database.
Why this rule exists:
Without database-per-service, you cannot deploy services independently. If the Order Service and Payment Service both query the orders table, a schema migration (renaming a column, changing a type, adding a constraint) requires coordinating both services. That coordination is a deployment dependency. Deployment dependencies are what microservices exist to eliminate.
The different database options:
Each service can choose the database technology that best fits its needs. The Order Service might use PostgreSQL for relational data with ACID transactions. The Catalog Service might use Elasticsearch for full-text search. The Recommendation Service might use a graph database. The Session Service might use Redis. This technology independence is a direct benefit of database-per-service.
Interview Relevance: Drawing separate databases for each service on your whiteboard diagram is the first signal to an interviewer that you understand microservices data ownership. Drawing multiple services pointing to one database immediately raises concerns.
SLIDE 68: The Shared Database Anti-Pattern
The shared database anti-pattern is the single most common architectural mistake in microservices. It occurs when multiple services read from or write to the same database, even if they access different tables.
Why teams fall into this pattern:
- “It is easier to just query the data directly than to build an API”
- “We will separate the databases later when we have time”
- “We only read from their tables, we do not write, so it is fine”
- “It is the same PostgreSQL instance, but different schemas, so it is separated”
Every one of these justifications leads to the same outcome: coupled services that cannot deploy independently.
The three levels of shared database coupling:
- Same table, same schema - Multiple services read and write the same tables. This is the worst case. Any schema change is a cross-team coordination nightmare.
- Same database, different tables - Services use their own tables but in the same database instance. A database outage or resource contention affects all services simultaneously. Schema migrations lock the database for all services.
- Same instance, different schemas - Better isolation, but still shares compute resources, connection pools, and availability. A runaway query from one service can starve another service’s connections.
Interview Relevance: If an interviewer describes a system where services share a database and asks “what would you change?”, this is the first thing to fix. Separating databases is the foundation that every other microservices benefit depends on.
SLIDE 68B: Shared Database, How It Breaks Independent Deployment
Scenario: An e-commerce platform has an Order Service and a Reporting Service. Both query the orders table in the same PostgreSQL database.
The breaking change:
The Order Service team needs to split the shipping_address column (a single text field) into separate columns: street, city, state, zip_code, country. This improves address validation and shipping cost calculation.
What happens with a shared database:
- The Order Service team writes a migration to add the new columns and populate them from the old
shipping_addressfield - They cannot drop the old
shipping_addresscolumn because the Reporting Service reads it - They ask the Reporting Service team to update their queries first
- The Reporting Service team is in the middle of a different project and cannot prioritize this for 3 weeks
- The Order Service team waits 3 weeks to ship their feature, or maintains both the old and new columns indefinitely
What happens with database-per-service:
- The Order Service team changes their internal schema however they want
- The Order Service API response still includes
shipping_addressfor backward compatibility (Section 4, contract versioning) - The Reporting Service continues consuming the API. It is not affected.
- The Order Service team ships their feature the same day
The lesson: Database-per-service does not just prevent technical coupling. It prevents organizational coupling. Teams can move at their own pace without waiting for other teams to adapt to internal changes.
Interview Relevance: This specific scenario (internal schema change blocked by another service’s direct database access) is a powerful concrete example to use in interviews when explaining why database-per-service matters.
SLIDE 69: Private Schema Ownership
Once each service has its own database, the next question is: what data goes where?
The ownership rule: A service owns the data that it is the system of record for. The service that creates and modifies a piece of data owns it. Other services can have copies, but only one service is the authoritative source of truth.
Practical examples:
- The User Service owns user profiles (name, email, account status). It is where user profiles are created and modified. Other services may cache the user’s name, but the User Service is the authority.
- The Order Service owns orders. It is where orders are created, updated, and cancelled. The Reporting Service may have a copy of order data for analytics, but the Order Service is the source of truth.
- The Inventory Service owns stock levels. It is where inventory is reserved and updated. The Catalog Service may display “in stock” on the product page, but it gets that information from the Inventory Service.
What the owning service must provide:
- An API for other services to query the data they need (synchronous)
- Events published when the data changes, so other services can maintain their own copies (asynchronous)
- A contract that is versioned and backward compatible so consumers are not broken by internal changes
What the owning service must protect:
- No direct database access from other services. The database credentials are known only to the owning service.
- No exposing internal schema details in the API. The API response structure can differ from the database schema.
- No allowing other services to bypass the API with “just a quick query.” There are no exceptions.
Interview Relevance: When you define service boundaries in an interview, explicitly stating which service is the “system of record” for each entity shows you have thought about data ownership, not just service naming.
SLIDE 70: Cross-Service Reads with API Composition
The most straightforward way to get data from another service is to call its API. When a single user request needs data from multiple services, you compose the response by calling each service and combining the results.
Example: Product page in an e-commerce platform

The API Gateway (or a Backend-for-Frontend layer) calls four services in parallel and assembles the product page response from their individual responses.
When API composition works well:
- The data is needed in real-time and must be fresh (e.g., current stock level, current price)
- The number of services to call is small (2-4 calls)
- The downstream services are fast and reliable
- The call pattern is simple (parallel fan-out, not sequential chains)
When API composition breaks down:
- Too many calls. Assembling a dashboard that needs data from 10 services adds latency and fragility. One slow service delays the entire response.
- Data needs joining. Displaying “top 10 products by revenue” requires joining order data with product data. Neither service has both datasets.
- High volume. If the product page gets 100,000 requests per second and each request triggers 4 downstream calls, the downstream services receive 400,000 requests per second. The fan-out multiplies load.
When API composition breaks down, materialized views (next slide) are the alternative.
Interview Relevance: API composition is the simplest cross-service read pattern. Start with it in interviews and move to materialized views only when you can articulate why composition is not sufficient.
SLIDE 71: Cross-Service Reads with Materialized Views
When API composition is too slow, too fragile, or requires joining data across services, a materialized view provides an alternative. The consuming service builds and maintains its own pre-computed copy of the data it needs.

How it works:
- The Catalog Service, Pricing Service, and Inventory Service each publish events when their data changes
- The Product Page Service consumes all three event streams
- It builds a denormalized read model that contains product name, current price, and stock status in a single database row
- When a client requests the product page, the Product Page Service queries its own local database. Zero cross-service calls at read time.
The trade-off:
- Reads are fast. One local database query instead of 4 network calls.
- Reads are resilient. If the Catalog Service is down, the Product Page Service still serves data from its local copy.
- Data is eventually consistent. When the Pricing Service updates a price, there is a delay (typically milliseconds to seconds) before the Product Page Service’s read model reflects the change.
- Storage is duplicated. Product data exists in the Catalog Service database and in the Product Page Service’s read model.
When to use materialized views:
- Read volume is much higher than write volume (product pages are viewed millions of times, but prices change thousands of times)
- You need data from 3+ services combined in a single query
- Latency requirements are strict and fan-out adds too much delay
- Slight staleness (seconds) is acceptable
Interview Relevance: Proposing a materialized view in an interview shows you understand that microservices trade data freshness for read performance and resilience. This is a senior-level pattern.
SLIDE 71B: Interview Practice
Question:
You are building a search page for an e-commerce platform. The search results need to show: product name (from Catalog Service), current price (from Pricing Service), average rating (from Reviews Service), and stock status (from Inventory Service). The search page receives 50,000 requests per second. How would you design the data access for this page?
Take 60 seconds to design your approach before reading below.
Weak Answer:
“The search page calls all four services for each request. We can parallelize the calls so it is fast.”
Why this is weak: At 50,000 requests per second, each downstream service receives 50,000 calls just from search. The Pricing Service, Inventory Service, and Reviews Service also serve their own direct traffic. The combined load is unsustainable. One slow service makes all search results slow. One failing service makes search unavailable.
Strong Answer:
“At 50,000 requests per second, API composition would overwhelm the downstream services and add unacceptable latency. I would use a materialized view approach.
I would build a Search Service with its own denormalized data store, likely Elasticsearch for full-text search capability. The Search Service consumes events from all four upstream services: ProductUpdated from Catalog, PriceChanged from Pricing, RatingUpdated from Reviews, and StockUpdated from Inventory. It maintains a search index where each document contains the product name, price, rating, and stock status together.
When a user searches, the Search Service queries its own local Elasticsearch index. Zero cross-service calls at query time. The search index updates within seconds of any upstream change, which is acceptable for a search page where slight staleness does not affect the user experience.
The trade-off is data duplication and eventual consistency. Product data exists in five places: Catalog DB, Pricing DB, Reviews DB, Inventory DB, and the Search index. But at 50,000 QPS, this is the only pattern that provides the performance and resilience we need.”
What makes this strong: The candidate explains why API composition fails at this scale, proposes a concrete alternative with the right technology (Elasticsearch), explains the event-driven update mechanism, and acknowledges the trade-offs explicitly.
SLIDE 72: Cross-Service Writes
Reading data across services is solved by API composition or materialized views. Writing data across services is fundamentally harder because you need changes in multiple databases to be coordinated.
The problem:
When a customer places an order, three things must happen: the Order Service creates the order, the Inventory Service reserves the stock, and the Payment Service authorizes the charge. In a monolith, this is one database transaction. In microservices, these are three separate databases with no shared transaction.
What happens if the order is created, the inventory is reserved, but the payment authorization fails? You now have an order in the database with reserved inventory but no payment. The data is inconsistent across services.
The approaches to cross-service writes:
- Synchronous orchestration - A central coordinator calls each service in sequence and handles failures by calling compensating actions (refund payment, release inventory, cancel order). This is the saga pattern, covered in depth in Section 6.
- Event-driven choreography - Each service publishes an event when it completes its step. The next service reacts to that event. No central coordinator. Also covered in Section 6.
- Avoid cross-service writes entirely - Redesign the boundaries so that the data that must change together lives in the same service. Sometimes the right answer is not a clever distributed write pattern but a better service boundary.
The key insight from Section 3: The aggregate boundary (Slide 27) defines what must change in one transaction. Everything inside an aggregate is a local write within one service. Everything that crosses an aggregate boundary is a cross-service write that requires sagas or events. Good aggregate design minimizes the number of cross-service writes you need.
Interview Relevance: When you describe a write operation that spans multiple services, the interviewer expects you to immediately address consistency. Saying “the Order Service creates the order and calls the Payment Service” without addressing failure scenarios is a gap that interviewers will probe.
SLIDE 73: Data Duplication as a Trade-Off
In microservices, data duplication is not a mistake. It is a deliberate architectural trade-off that enables service independence. This is one of the hardest mindset shifts for engineers who come from a relational database background where normalization is a core principle.
Why duplication is necessary:
In a monolith, you normalize data to avoid redundancy. The product name exists in one row of the products table. Every query that needs the product name joins to that table. This works because everything is in one database.
In microservices, the products table is in the Catalog Service database. The Order Service cannot join to it. When the Order Service creates an order, it needs to store the product name and price at the time of purchase. It copies that data into its own order_items table. The product name now exists in two databases.
When duplication is correct:
- The Order Service stores “product name at time of purchase” because the product name might change later. The order record should reflect what the customer bought, not the current product name.
- The Search Service stores a denormalized copy of product data for fast querying. The source of truth remains the Catalog Service.
- The Notification Service stores the customer’s email address so it can send emails without calling the User Service for every notification.
When duplication is a problem:
- If two services both consider themselves the source of truth for the same data and both accept writes, you have a conflict resolution problem, not a duplication strategy.
- If duplicated data is never refreshed and becomes stale in ways that affect business correctness (e.g., the price in the order does not match what the customer was shown).
The rule: One service is the source of truth. Other services hold copies that are either snapshotted at a point in time (order capturing price at purchase) or kept updated through events (search index consuming price change events).
Interview Relevance: Saying “I would duplicate the product name in the order record because the order should capture the state at purchase time” shows the interviewer you understand why duplication exists in microservices, not just that it happens.
SLIDE 74: Eventual Consistency in Practice
In a monolith with one database, every read sees the latest write. In microservices with separate databases and event-driven updates, there is a delay between when data changes in one service and when other services reflect that change. This is eventual consistency.
How eventual consistency manifests:
- A seller updates a product price from 24.99 in the Catalog Service
- The Catalog Service publishes a PriceChanged event
- The Search Service consumes the event and updates its index
- For a few hundred milliseconds (or a few seconds under load), the product page shows 29.99
Is this acceptable? Usually, yes. Most users never notice a sub-second inconsistency between two different pages. The few who do will see the correct price when they refresh. This is a drastically different situation from a banking system where an account balance must be immediately consistent.
When eventual consistency is not acceptable:
- Financial transactions where the user’s balance must reflect a debit immediately
- Inventory checks where selling an out-of-stock item is costly (overselling)
- Security-sensitive operations where permission changes must take effect immediately
For these cases, use synchronous calls to the source-of-truth service rather than relying on an eventually consistent copy. This trades performance for correctness.
Typical consistency delays:
- Event stream consumption (Kafka): 10-500ms under normal load
- Materialized view update: 100ms-2 seconds depending on processing
- Search index update (Elasticsearch): 1-5 seconds (near real-time refresh)
- Cache TTL expiry: depends on configuration, commonly 30 seconds to 5 minutes
Interview Tip: When you propose eventual consistency in an interview, always specify which interactions can tolerate it and which cannot. “Search results can be eventually consistent, but the checkout price must come from the source of truth in real-time” shows you understand the nuances.
Interview Relevance: Every microservices interview eventually reaches the consistency question. Having a clear framework for when eventual consistency is acceptable versus when strong consistency is required is essential.
SLIDE 75: The Read-Your-Writes Problem
This is the most common consistency problem that users actually notice. A user makes a change and immediately sees stale data because their read hits a replica or a cached copy that has not been updated yet.
The classic scenario:
- A user updates their profile name from “John” to “Jonathan” in the User Service
- The User Service writes to the primary database
- The user immediately navigates to their profile page
- The profile page reads from a replica or a cached copy that still shows “John”
- The user thinks the update failed
Solution: Read-your-writes consistency
After a user writes data, route their subsequent reads to the source of truth (primary database or owning service) for a short window. Other users can continue reading from replicas or caches with no issue.
def update_profile(user_id, new_name):
primary_db.update_user(user_id, name=new_name)
cache.set(f"recent_write:{user_id}", True, ttl=5)
def get_profile(user_id, requesting_user_id):
if requesting_user_id == user_id and cache.get(f"recent_write:{user_id}"):
return primary_db.get_user(user_id)
return replica_db.get_user(user_id)
After a user updates their profile, a flag is set in the cache for 5 seconds. During those 5 seconds, that specific user’s profile reads are routed to the primary database. Every other user reads from the replica as usual. After 5 seconds, the replica has caught up, and the flag expires.
The key insight: You only need strong consistency for the user who just wrote. Every other user can tolerate eventual consistency and never notice. This gives you the performance benefits of replicas and caches for 99.9% of reads while guaranteeing that the writing user sees their own changes immediately.
Interview Relevance: Read-your-writes consistency is a specific, named pattern that interviewers expect senior candidates to know. It demonstrates understanding of consistency beyond just “strong” or “eventual.”
SLIDE 76: CQRS
CQRS (Command Query Responsibility Segregation) separates the write model (commands) from the read model (queries) into different data stores optimized for their respective access patterns.

Why separate read and write models?
In many systems, the ideal data structure for writing is different from the ideal structure for reading. The write side needs normalization, constraints, and ACID transactions to maintain correctness. The read side needs denormalization, pre-computed aggregations, and fast lookups to serve queries quickly.
Example: Order history page
The write model stores orders in normalized tables: orders, order_items, shipping_addresses, payment_records. Writes enforce referential integrity and business rules.
The read model stores a denormalized order summary: order ID, customer name, total amount, item count, order status, and last updated timestamp in a single document. Reads serve the order history page with one query instead of joining four tables.
When CQRS is justified:
- Read and write volumes differ by 10x or more (far more reads than writes)
- The read model needs a different database technology than the write model (e.g., writes to PostgreSQL, reads from Elasticsearch)
- The read model needs different data shapes for different consumers (mobile app needs a summary, admin dashboard needs full details)
When CQRS is overkill:
- Read and write patterns are similar (basic CRUD with similar volume)
- The system is small enough that one database handles both efficiently
- The added complexity of maintaining two models and event synchronization is not justified by the performance gain
Interview Relevance: CQRS is a pattern that interviewers expect you to know but also expect you to justify. Proposing CQRS for a simple CRUD service signals over-engineering. Proposing it for a high-read system with complex query needs signals strong design sense.
SLIDE 76B: CQRS, Worked Example
Scenario: A food delivery platform like DoorDash needs to display a restaurant’s menu page and also allow restaurant owners to update their menus.
Write Side (Restaurant Management Service):
Restaurant owners update menus through an admin panel. The write model is normalized and relational:
CREATE TABLE restaurants (
restaurant_id UUID PRIMARY KEY,
name VARCHAR(255),
cuisine_type VARCHAR(100),
address TEXT
);
CREATE TABLE menu_items (
item_id UUID PRIMARY KEY,
restaurant_id UUID REFERENCES restaurants(restaurant_id),
name VARCHAR(255),
description TEXT,
price DECIMAL(10,2),
category VARCHAR(100),
available BOOLEAN DEFAULT TRUE
);
Write volume: a few hundred menu updates per day across all restaurants. Writes need validation (price must be positive, category must be valid) and transactional consistency (adding an item and updating the restaurant’s item count must be atomic).
Read Side (Menu Display Service):
Customers browse menus on the app. The read model is denormalized and optimized for fast retrieval:
{
"restaurant_id": "rest_123",
"name": "Mario's Pizza",
"cuisine": "Italian",
"menu": {
"Appetizers": [
{ "name": "Bruschetta", "price": 8.99, "available": true },
{ "name": "Calamari", "price": 12.99, "available": false }
],
"Mains": [{ "name": "Margherita Pizza", "price": 14.99, "available": true }]
},
"total_available_items": 15,
"average_price": 16.5
}
Read volume: millions of menu views per day. Each read returns the full menu with pre-computed aggregations (total items, average price) in one query. No joins needed.
The sync mechanism: When a restaurant owner updates a menu item, the write service publishes a MenuItemUpdated event. The read service consumes the event and rebuilds the denormalized menu document. Typical lag: under 1 second.
Interview Relevance: Walking through a concrete CQRS example with actual schemas and data shapes demonstrates that you can implement the pattern, not just name it.
SLIDE 76C: Interview Practice
Question:
An interviewer asks: “Your e-commerce platform has a product catalog with 10 million products. The catalog page gets 200,000 reads per second. Product managers update about 5,000 products per day. How would you design the data layer?”
Take 60 seconds to design your approach before reading below.
Weak Answer:
“I would use a PostgreSQL database with read replicas to handle the read volume. The replicas can serve the 200,000 reads per second.”
Why this is weak: PostgreSQL replicas can scale reads, but each read still requires joining product, pricing, and inventory tables. At 200,000 QPS with multi-table joins, even replicas will struggle. The answer also does not address the fact that the read model (what customers see) is very different from the write model (what product managers edit).
Strong Answer:
“The read-to-write ratio here is extreme: 200,000 reads per second versus about 0.06 writes per second (5,000 per day). This is a textbook case for CQRS.
For the write side, I would use PostgreSQL with a normalized schema. Product managers update products through an admin API. Writes enforce business rules (valid categories, positive prices, required images) and are transactionally consistent. At 5,000 writes per day, a single PostgreSQL instance handles this easily.
For the read side, I would use a denormalized data store optimized for fast reads. Each product document contains everything needed to render the catalog page: product name, description, main image URL, current price, stock status, and average rating. At 200,000 QPS, I would use a combination of Elasticsearch for search queries and Redis for individual product lookups, both populated from events published by the write side.
When a product manager updates a product, the write service publishes a ProductUpdated event. The read services consume it and update their denormalized stores. Typical lag is under 1 second, which is completely acceptable for catalog browsing.
This gives us fast reads with no joins, a clean write model with proper validation, and independent scaling of the read and write sides.”
What makes this strong: The candidate calculates the read-to-write ratio, justifies CQRS with specific numbers, chooses appropriate technologies for each side, and explains the synchronization mechanism.
SLIDE 77: Materialized Views and Denormalization
Materialized views are pre-computed query results stored in a read-optimized format. In microservices, they are the primary tool for making cross-service data available without runtime API calls.
How materialized views work in microservices:
The source services (Catalog, Pricing, Inventory) publish events when their data changes. A consuming service listens to all relevant event streams and builds a combined, denormalized view in its own database. The view is “materialized” because it is physically stored, not computed on-the-fly.
Denormalization across services:
In a monolith, the order history query joins the orders, products, users, and payments tables. In microservices, each of those tables belongs to a different service. The Order History view denormalizes all of this into a single document per order:
- Order ID, status, created date (from Order Service events)
- Product names and prices at time of purchase (snapshotted when the order was created)
- Customer name and email (from User Service events)
- Payment status and method (from Payment Service events)
One query returns everything needed to display the order history. No cross-service calls.
Keeping materialized views fresh:
- The view rebuilds automatically as events arrive
- If an event is missed or processing fails, the consumer replays from the last known offset in the event stream
- Periodic full rebuilds can be scheduled to catch any drift
The cost of materialized views:
- Storage duplication (the same data exists in the source service and the view)
- Processing overhead (consuming events, updating the view)
- Eventual consistency (the view lags behind the source by milliseconds to seconds)
- Increased system complexity (event schemas must be maintained, consumers must handle out-of-order events)
Interview Relevance: Materialized views are the go-to solution for cross-service reads at scale. Knowing when to use them and how they are kept fresh is expected in senior-level interviews.
SLIDE 78: Ownership of Reference Data
Reference data is data that many services need but only one service should own. Product names, user profiles, currency codes, country lists, and pricing tiers are all reference data. Handling it correctly prevents the most common data ownership conflicts.
Pattern 1: Snapshot at event time
The Order Service needs the product name and price when creating an order. It captures a snapshot of this data at the moment of purchase and stores it in its own database. The snapshot never changes, even if the product name or price changes later. The Order Service does not consume ongoing product events.
This is appropriate for data that must reflect a point in time (what the customer actually bought and paid).
Pattern 2: Event-driven local cache
The Notification Service needs the customer’s email address to send emails. It consumes UserUpdated events from the User Service and maintains a local lookup table of user IDs to email addresses. When it needs to send an email, it queries its local table instead of calling the User Service.
This is appropriate for reference data that should stay current but does not need real-time freshness.
Pattern 3: Synchronous API call
The Checkout Service needs the customer’s current default payment method right now, at checkout time. It calls the Payment Service API synchronously because this data must be fresh (the customer may have just changed their payment method) and stale data would cause a payment failure.
This is appropriate for data where staleness causes business errors.
Choosing between patterns:
- Does the data need to reflect a point in time? → Snapshot
- Does the data need to be roughly current but can tolerate seconds of staleness? → Event-driven local cache
- Does staleness cause business failures? → Synchronous API call
Interview Relevance: Reference data ownership is a practical problem that comes up in every real microservices system. Having three named patterns with clear selection criteria makes your interview answers specific and actionable.
SLIDE 79: Search Indexing as a Derived Read Model
Search is almost always a cross-service concern. A search query like “red shoes under $50 in stock near me” touches data from the Catalog Service (product attributes), Pricing Service (price), Inventory Service (stock levels), and Location Service (warehouse proximity). No single service owns all of this data.

The pattern:
A Search Service owns a search index (typically Elasticsearch or OpenSearch). An indexer process consumes events from all relevant upstream services and maintains the search index. Each document in the index is a denormalized product record containing all searchable attributes.
This is a materialized view specifically optimized for search. It supports full-text search, faceted filtering, relevance ranking, and geospatial queries that no individual service’s database can handle efficiently.
Who owns the Search Service?
The Search Service owns the index, the indexer, and the search API. It does not own the source data. The Catalog Service is still the authority for product names. The Pricing Service is still the authority for prices. The Search Service is a derived read model that assembles and indexes data from multiple sources.
Consistency considerations:
A price change takes 1-5 seconds to appear in search results. This is acceptable because search results show approximate information. The actual price is always confirmed from the source of truth (Pricing Service) at checkout time.
Interview Relevance: Search is one of the most common features in system design interviews. Explaining how the search index is populated from multiple services via events demonstrates a deep understanding of cross-service data patterns.
SLIDE 80: Cache Ownership and Invalidation
Caching in microservices raises a specific ownership question: which service is responsible for caching data, and which service is responsible for invalidating the cache when the data changes?
The ownership rule: The service that owns the data owns the cache invalidation logic. Other services that cache a copy of the data should follow the owning service’s signals (events, TTLs) to invalidate their caches.
Pattern 1: Source-owned cache
The Catalog Service caches its own product data in Redis. When a product is updated, the Catalog Service invalidates or updates the cache entry. Consumers of the Catalog API automatically get fresh data because the source manages the cache.
This is the simplest pattern and should be the default.
Pattern 2: Consumer-owned cache with TTL
The Order Service caches product names locally to avoid calling the Catalog Service for every order display. The cache has a 60-second TTL. After 60 seconds, the next request fetches fresh data from the Catalog API.
This is appropriate when slight staleness is acceptable and the consuming service wants to reduce its dependency on the source service.
Pattern 3: Consumer-owned cache with event invalidation
The Search Service caches product data and consumes ProductUpdated events from the Catalog Service. When an event arrives, the Search Service invalidates or updates the relevant cache entry immediately.
This provides fresher data than TTL-based caching at the cost of consuming and processing the event stream.
The cardinal sin of cache ownership: Service A caches data from Service B, and Service B has no idea. When Service B’s data changes, Service A’s cache remains stale with no invalidation mechanism. This causes bugs that are extremely difficult to diagnose because the stale data looks correct until you compare it with the source.
Interview Tip: When you describe caching in an interview, always state who owns the invalidation. “The Catalog Service caches product data and invalidates on write” is a complete answer. “We cache product data” leaves the interviewer wondering who invalidates it.
Interview Relevance: Cache invalidation is famously one of the hardest problems in computer science. In microservices, it is even harder because the cache and the data source live in different services. Showing you have a clear ownership model for invalidation is a strong signal.
SLIDE 81: Reporting and Analytics
Operational databases (the databases each microservice owns) are optimized for transactional workloads: fast reads and writes for individual records. Analytical workloads (aggregations, trends, cross-domain reports) have very different requirements and should not run against operational databases.
The problem:
A business analyst wants to run: “Show me total revenue by product category for the last 90 days.” This query touches order data (Order Service), product categories (Catalog Service), and payment amounts (Payment Service). Running this aggregation across three microservice APIs would be slow, resource-intensive, and could degrade the operational services for real users.
The solution: Operational database vs analytical store
Each microservice publishes domain events to an event stream. An ETL (Extract, Transform, Load) pipeline or stream processor consumes these events and loads them into a centralized analytical store: a data warehouse (like BigQuery, Snowflake, or Redshift) or a data lake.
The analytical store contains copies of data from all services in a format optimized for aggregation, joining, and trend analysis. Business analysts query the analytical store directly. They never touch the operational databases.
How this preserves service independence:
- Services only publish events. They do not need to support reporting queries.
- The reporting schema can differ from any service’s internal schema.
- Adding new reporting dimensions does not require changing any service.
- Heavy analytical queries do not affect operational service performance.
Interview Relevance: When an interviewer asks about reporting or analytics in a microservices system, the expected answer is a separate analytical store fed by events. Running analytical queries against operational microservice databases is a design mistake.
SLIDE 81B: Note It Down
Seven data ownership patterns to remember:
- Database per service - Non-negotiable. Each service owns its data store. No shared databases.
- API composition - Call multiple services and combine results. Best for real-time data from 2-4 services.
- Materialized views - Pre-compute cross-service data from events. Best for high-read, cross-service queries.
- CQRS - Separate write model from read model. Best when read/write patterns differ dramatically.
- Event-driven local cache - Consume events to maintain a local copy. Best for reference data that should stay current.
- Snapshot at event time - Capture data at the moment of use. Best for point-in-time records (order capturing price at purchase).
- Analytical store - Centralized warehouse fed by events. Best for reporting and cross-domain analytics.
The selection rule: Start with the simplest pattern (API composition). Move to more complex patterns (materialized views, CQRS) only when you can articulate a specific problem that the simpler pattern does not solve (too many calls, too much latency, read/write ratio too skewed).
Interview Relevance: Being able to name these patterns and explain when each is appropriate puts you ahead of most candidates, who only know “each service has its own database” without knowing how to handle the cross-service data problems that creates.
SLIDE 82: Schema Migration Across Services
In a monolith, a schema migration is one coordinated change. In microservices, schema changes in one service must not break other services that consume its API or events.
The expand-contract pattern:
This is the standard approach for making schema changes safely across independently deployed services.
Phase 1: Expand
Add the new field alongside the old field. Populate both. The API and events include both the old and new fields. Existing consumers continue using the old field. New consumers can start using the new field.
Phase 2: Migrate
All consumers switch to using the new field. This happens service by service, each deploying at their own pace. No coordination required.
Phase 3: Contract
Once all consumers have migrated, remove the old field from the API and events. Stop populating it. Deploy the owning service.
Example: Renaming “shipping_address” to structured address fields
Phase 1: The Order Service API returns both shipping_address (old string field) and street, city, state, zip, country (new structured fields). Phase 2: The Reporting Service, the Shipping Service, and the Analytics pipeline each update to use the new structured fields, deploying independently. Phase 3: After confirming all consumers have migrated (contract tests pass without the old field), the Order Service removes shipping_address from the API.
Timeline: This process typically takes 2-4 weeks, not because the technical work is complex, but because each consuming team deploys on their own schedule.
Interview Relevance: Describing the expand-contract pattern shows the interviewer you understand how schema changes work in a system where services deploy independently. It connects directly to the backward compatibility principles from Section 4.
SLIDE 82B: Interview Practice
Question:
You have designed a microservices system for an e-commerce platform. The interviewer asks: “The product detail page needs data from five different services. How do you avoid making five API calls for every page view? And what happens to the product page if one of those services goes down?”
Take 60 seconds to think about your answer before reading below.
Weak Answer:
“I would call all five services in parallel. Parallel calls are fast. If one service is down, I would show a degraded page without that data.”
Why this is weak: Parallel calls help latency but not reliability. At 100,000 page views per second, each service receives 100,000 calls from the product page alone. The answer also does not address how the “degraded page” works in practice.
Strong Answer:
“I would not make five API calls per page view. Instead, I would build a Product Page Service that maintains a materialized view combining data from all five upstream services.
The Product Page Service consumes events from the Catalog Service (product details), Pricing Service (current price), Inventory Service (stock status), Reviews Service (ratings), and Promotion Service (active discounts). It stores a denormalized product document in its own database with all five data points in a single record.
When a client requests a product page, the Product Page Service queries its own local database. One query, zero cross-service calls. At 100,000 QPS, this is the only pattern that scales.
For resilience: if the Catalog Service goes down, the Product Page Service is unaffected because it reads from its own database. The only impact is that product updates stop flowing until the Catalog Service recovers. But existing products continue to be served with the last known data, which is perfectly fine for a product page.
The trade-off is eventual consistency. A price change takes 1-2 seconds to appear on the product page. This is acceptable because the actual price is always confirmed from the Pricing Service at checkout time, not from the product page cache.”
What makes this strong: The candidate proposes a concrete architecture (materialized view), explains the event-driven update mechanism, addresses both performance and resilience, and acknowledges the consistency trade-off with a mitigation strategy (confirming price at checkout).
SLIDE 83: Data Patterns Decision Framework
When you need data from another service, use this decision tree.
Question 1: Is this a read or a write?
- Read → Continue to Question 2
- Write across services → Saga pattern (Section 6)
Question 2: How many services are involved?
- 1-2 services, low volume → API composition (call their APIs directly)
- 3+ services, or high volume → Materialized view or CQRS
Question 3: Does the data need to be real-time fresh?
- Yes, staleness causes business errors (e.g., current balance, inventory at checkout) → Synchronous API call to the source of truth
- No, seconds of staleness is acceptable (e.g., search results, product catalog, dashboards) → Materialized view or event-driven cache
Question 4: Are read and write patterns dramatically different?
- Yes (reads 100x-1000x more than writes, or reads need a different data shape) → CQRS
- No (similar read/write volume and shape) → Single model is sufficient
Question 5: Is this for reporting or analytics?
- Yes → Analytical store (data warehouse) fed by events. Never query operational databases for analytics.
Interview Relevance: Walking through this decision tree in an interview, arriving at a pattern through reasoning rather than stating it from memory, demonstrates the kind of systematic thinking interviewers want to see.
SLIDE 84: Section Summary and Interview Tips
Section 5 Summary:
Common Interview Mistakes:
Key Numbers to Remember:
How This Connects to Other Sections:
Section 6 (Sagas and Consistency) addresses the cross-service write problem that this section identified but deferred. When two services both need to update their data as part of one business operation, sagas coordinate the workflow with compensating transactions for failures. Section 7 (API Gateway) builds on the API composition pattern. The gateway or BFF layer is often where API composition happens, aggregating responses from multiple backend services before returning to the client. Section 8 (Reliability) extends the eventual consistency discussion into system-level observability, showing how to monitor consistency lag and detect when materialized views fall behind.