System Design Fundamentals & Framework
How to think like an architect, HLD vs LLD, functional vs non-functional constraints, latency vs throughput, and the 9-step interview framework.
A battle-tested, 6-day interview curriculum covering core mental models, horizontal scalability, database sharding, Redis caching, Kafka messaging, and REST API design — complete with verbal interview scripts, ASCII architecture blueprints, and an interactive 3D flashcard study engine.
Progress is automatically preserved in your browser.
Work through the track in order, or jump directly into any architecture topic.
How to think like an architect, HLD vs LLD, functional vs non-functional constraints, latency vs throughput, and the 9-step interview framework.
Horizontal vs vertical scaling, Layer 4 vs Layer 7 load balancers, health checks, stateless architecture, sticky sessions, and eliminating single points of failure (SPOF).
Database scaling bottlenecks, ACID vs BASE, read replicas, replication lag, horizontal sharding strategies, and consistent hashing.
Cache-aside, write-through, write-back, TTL expiration, cache invalidation, LRU/LFU eviction, and preventing cache stampede, penetration, and avalanche.
Decoupling distributed systems, Kafka vs RabbitMQ vs Azure Service Bus, at-least-once delivery, idempotency, dead-letter queues, and the Transactional Outbox pattern.
REST architectural constraints, HTTP status codes, offset vs keyset/cursor pagination, token bucket rate limiting, idempotency keys, and zero-downtime API versioning.
Test your verbal recall and architecture intuition. Flip each card to inspect the concise interview delivery script.
...
Instant full-text search across all 60 interview questions, architectural answers, and C# examples.
System design is about deciding how different components work together to build a scalable, reliable, and maintainable system.
Interview answer:
System design is the process of defining the architecture, components, data storage, APIs, communication patterns, and infrastructure of a software system based on its requirements.
The goal is not only to make the application function correctly, but also to ensure it can handle requirements such as scalability, availability, reliability, performance, security, and maintainability.
For example, if we're designing an e-commerce application, we need to decide how clients communicate with backend services, where product and order data is stored, how frequently accessed data is cached, how requests are distributed across servers, and how the system behaves when a component fails.
Key sentence to remember:
System design is about deciding how different components work together to build a scalable, reliable, and maintainable system.
GetProductAsync(int id);
Interview answer:
HLD, or High-Level Design, describes the overall architecture of the system and the major components and their interactions.
For example:
Client
↓
API Gateway
↓
Backend Services
↓
Cache / Database / Message QueueIn HLD, we discuss things such as:
LLD, or Low-Level Design, focuses on the internal implementation of individual components.
For example, in a .NET application:
ProductController
↓
IProductService
↓
ProductService
↓
IProductRepository
↓
DatabaseLLD includes:
public interface IProductService
{
Task<Product?> GetProductAsync(int id);
}It covers classes, interfaces, relationships, SOLID principles and design patterns.
Key difference:
HLD explains how the overall system is structured, while LLD explains how individual components are implemented.
"The user should be able to place an order."
Interview answer:
Functional requirements describe what the system should do.
For an e-commerce application, functional requirements could be:
Search products
Add product to cart
Place order
Make payment
Track orderNon-functional requirements describe how well the system should perform those functions.
Examples include:
Scalability
Availability
Reliability
Latency
Security
Consistency
Durability
MaintainabilityFor example:
"The user should be able to place an order."
is a functional requirement.
Whereas:
"The order API should respond within 300 milliseconds for 99% of requests."
is a non-functional requirement.
Remember:
Functional → WHAT the system does
Non-functional → HOW WELL the system does itScalability is the ability of a system to handle increasing workload by adding resources without significantly degrading performance.
Interview answer:
Scalability is the ability of a system to handle increasing workload by adding resources without significantly degrading performance.
For example, suppose our API initially handles:
1,000 requests/secbut after the application grows, it needs to handle:
100,000 requests/secThe architecture should allow us to increase capacity.
There are two main approaches:
Vertical Scaling
Horizontal ScalingVertical scaling means increasing the resources of an existing server.
Horizontal scaling means adding additional servers.
For distributed web applications, horizontal scaling is commonly preferred because it can provide both greater capacity and better fault tolerance.
"For a small application, vertical scaling may be sufficient and simpler. For a high-traffic distributed application, I'd generally design for horizontal scaling because we can add application instances as traffic increases and avoid depending on a single server."
Interview answer:
Vertical scaling means increasing the resources of an existing machine.
For example:
Before
4 CPU
8 GB RAM
↓
After
16 CPU
64 GB RAMThis is also called scaling up.
Horizontal scaling means adding more machines.
Load Balancer
/ | \
↓ ↓ ↓
Server 1 Server 2 Server 3This is called scaling out.
| Vertical Scaling | Horizontal Scaling |
|---|---|
| Bigger machine | More machines |
| Scaling up | Scaling out |
| Easier initially | More architectural complexity |
| Hardware has limits | Can scale much further |
| May retain single point of failure | Better fault tolerance when designed properly |
| Often requires larger-machine upgrades | Capacity can be added incrementally |
I'd say:
"For a small application, vertical scaling may be sufficient and simpler. For a high-traffic distributed application, I'd generally design for horizontal scaling because we can add application instances as traffic increases and avoid depending on a single server."
Availability refers to whether a system is accessible and able to serve requests when users need it.
Interview answer:
Availability refers to whether a system is accessible and able to serve requests when users need it.
For example:
99.9% availabilitymeans the system is expected to be available for 99.9% of the measured period.
Reliability refers to whether the system continues to perform its intended function correctly and consistently over time.
Consider a payment system.
Suppose the API is reachable almost all the time:
Availability = Highbut occasionally the application processes the same payment twice.
The system may be highly available, but it isn't sufficiently reliable or correct.
Availability
↓
Can I access the system?
Reliability
↓
Can I trust the system to work correctly?In real system design, these concepts overlap, but they're not identical.
Latency measures how long an operation takes, while throughput measures how many operations can be processed over time.
Interview answer:
Latency is the amount of time required to process a request or operation.
For example:
Client → API → Database → API → Client
Total = 120 msThe request latency is approximately:
120 msThroughput represents how much work the system processes within a given amount of time.
For an API, we might measure:
10,000 requests/secondFor a messaging system:
500,000 messages/secondFor storage:
MB/s or GB/sSo:
Latency measures how long an operation takes, while throughput measures how many operations can be processed over time.
This question often follows Q7.
This question often follows Q7.
Interview answer:
Latency measures the time required for an individual operation, whereas throughput measures the amount of work completed within a given period.
For example:
Average API response = 100 msThat's latency.
API handles 20,000 requests/secThat's throughput.
A system can theoretically have:
Low latency + Low throughput
High latency + High throughput
Low latency + High throughput ← usually desirableDon't assume improving throughput automatically improves latency.
For example, batch processing may increase throughput because many records are processed together, while increasing the latency experienced by an individual item waiting for a batch.
"Design an e-commerce system."
This is extremely important.
If an interviewer says:
"Design an e-commerce system."
Don't immediately say:
"I'll use microservices, Kafka, Redis, Kubernetes..."
That's a common mistake.
Start with requirements clarification.
Ask:
"What are the core features we need to support?"
For example:
Product search?
Cart?
Orders?
Payments?
Inventory?
Recommendations?Then clarify scale.
How many users?
How many daily active users?
What's the expected read/write traffic?
Are there traffic spikes?Then ask about non-functional requirements.
What availability do we require?
What latency is acceptable?
Do we need strong consistency?
Are there durability requirements?Then clarify data characteristics.
How much data?
How quickly does it grow?
What's the read/write ratio?
How long should we retain it?I'd say:
"Before choosing technologies, I'd first clarify the functional requirements, expected scale, traffic patterns, data volume, consistency requirements, latency expectations and availability requirements. Those constraints will drive the architecture."
That's a very good sentence to remember.
**Design a URL Shortener like Bitly.**
This is the most important question today because it tests whether you can apply the framework.
Suppose the interviewer says:
Design a URL Shortener like Bitly.
Don't immediately start designing.
I'd start by asking:
"Before getting into the architecture, I'd like to clarify the requirements and expected scale."
We need:
1. Create short URL
Long URL
↓
https://example.com/something/very/long
↓
Short URL
short.ly/Ab12CdSecond:
2. Redirect short URL
short.ly/Ab12Cd
↓
Original URLPotential additional requirements:
Custom aliases
Expiration
AnalyticsBut if the interviewer says they're out of scope, don't design them.
Suppose the interviewer gives:
10 million new URLs/day
100 million redirects/dayCalculate approximate average QPS.
For writes:
10,000,000 / 86,400
≈ 116 writes/secFor redirects:
100,000,000 / 86,400
≈ 1,157 reads/secBut average traffic isn't enough.
We should design for peak traffic, potentially several times higher.
Notice something else:
Reads >> WritesSo this is a read-heavy system.
That immediately suggests caching may be useful.
We could expose:
POST /api/urlsRequest:
{
"longUrl": "https://example.com/products/12345"
}Response:
{
"shortUrl": "https://short.ly/aB3x91"
}Redirect:
GET /aB3x91Response:
HTTP 301/302 Redirect
Location: https://example.com/products/12345We'd discuss 301 vs 302 later because it has caching and analytics implications.
A simple model might be:
UrlMapping
shortCode
longUrl
createdAt
expiresAtExample:
aB3x91
↓
https://example.com/products/12345The most important lookup is:
shortCode → longUrlTherefore shortCode should support efficient lookup, typically through an appropriate key/index.
Now we can finally draw the architecture.
┌───────────┐
│ Clients │
└─────┬─────┘
│
▼
┌─────────────┐
│Load Balancer│
└──────┬──────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│API │ │API │
│Server 1 │ │Server 2 │
└────┬────┘ └────┬────┘
│ │
└────────┬────────┘
│
┌─────▼─────┐
│ Cache │
└─────┬─────┘
│
Cache Miss
│
▼
┌──────────┐
│ Database │
└──────────┘User enters:
short.ly/aB3x91Request goes:
Client
↓
Load Balancer
↓
API Server
↓
CacheIf cache contains:
aB3x91
↓
https://example.com/products/12345return the redirect immediately.
This gives us:
Client
↓
API
↓
Cache
↓
RedirectVery fast.
If the mapping isn't cached:
API
↓
Cache
↓
MISS
↓
Database
↓
Get URL
↓
Store in Cache
↓
RedirectSubsequent requests can hit the cache.
Because URL shorteners are typically read-heavy, popular mappings are good caching candidates.
If traffic grows:
Load Balancer
/ | \
↓ ↓ ↓
API1 API2 API3Application servers should ideally remain stateless so instances can be added or removed easily.
We'll study stateless architecture later.
Potential bottlenecks include:
Database traffic
Hot URLs
Cache overload
Short-code generation
Storage growth
Server failuresFor example, if a celebrity posts:
short.ly/xyz123millions of people might access the exact same URL.
That's a hot key scenario.
Caching can reduce database pressure, though at very large scale we'd need to think more carefully about cache distribution and hot-key handling.
For example:
Don't say:
"NoSQL is better because it's faster."
Instead say:
"The dominant access pattern is a key-based lookup from shortCode to longUrl. Depending on the expected scale and consistency requirements, a distributed key-value store could be a good fit. At smaller scale, a relational database with an indexed shortCode can also work well. I'd choose based on the actual scale and operational requirements."
That's system-design thinking.
If the interviewer asks you to summarize your approach, say:
"I'd first clarify the functional and non-functional requirements. Then I'd estimate traffic and storage to understand the expected scale. Next I'd define the APIs and data model. After that I'd create the high-level architecture and walk through the major read and write flows. Finally, I'd identify bottlenecks and discuss scaling, caching, availability, failure handling and the trade-offs behind the design choices."
That's one of the most useful answers to remember.
System Design Interview
↓
1. Requirements
↓
2. Scale Estimation
↓
3. APIs
↓
4. Data Model
↓
5. High-Level Design
↓
6. Request/Data Flow
↓
7. Bottlenecks
↓
8. Scaling
↓
9. Failure Handling
↓
10. Trade-offsAnd remember these five pairs:
Functional = WHAT
Non-functional = HOW WELL
Vertical scaling = BIGGER machine
Horizontal = MORE machines
Latency = TIME per operation
Throughput = WORK per unit time
Availability = Can users access it?
Reliability = Does it keep working correctly?
HLD = Architecture
LLD = Classes / objects / implementationHorizontal scaling means increasing system capacity by adding more servers or application instances instead of increasing the resources of an existing server.
Horizontal scaling means increasing system capacity by adding more servers or application instances instead of increasing the resources of an existing server.
For example, instead of running one API server:
Users
↓
API Serverwe can run multiple instances:
Load Balancer
/ | \
↓ ↓ ↓
API 1 API 2 API 3It is often preferred for large-scale systems because we can add or remove instances as traffic changes. It also provides better fault tolerance because failure of one instance doesn't necessarily bring down the entire application.
If one server handles approximately 5,000 requests/sec:
1 server → 5K req/sec
3 servers → ~15K req/sec
10 servers → ~50K req/secThe actual capacity depends on the workload and other bottlenecks.
Interviewer: "Can a monolithic application be horizontally scaled?"
Yes. Horizontal scaling doesn't require microservices. An ASP.NET Core monolith can run as multiple stateless instances behind a load balancer.
That's an important point.
Vertical scaling means increasing the CPU, RAM, storage, or other resources of an existing machine. Horizontal scaling means adding more machines or application instances.
Vertical scaling means increasing the CPU, RAM, storage, or other resources of an existing machine. Horizontal scaling means adding more machines or application instances.
Before:
4 CPU + 8 GB RAM
↓
After:
16 CPU + 64 GB RAM Load Balancer
/ | \
↓ ↓ ↓
API 1 API 2 API 3| Vertical | Horizontal | |
|---|---|---|
| Approach | Bigger machine | More machines |
| Also called | Scale up | Scale out |
| Complexity | Lower initially | Higher |
| Hardware limit | Yes | Much higher |
| Fault tolerance | Limited | Better when designed correctly |
| Elasticity | Limited | Better |
| Typical large-scale architecture | Less preferred | Common |
"For a small application, vertical scaling may be simpler. For a high-traffic application, I'd generally design for horizontal scaling because it provides greater capacity and allows us to add or remove instances based on demand."
A load balancer distributes incoming requests across multiple backend servers or instances.
A load balancer distributes incoming requests across multiple backend servers or instances.
For example:
Users
│
▼
┌─────────────┐
│Load Balancer│
└──────┬──────┘
┌──────┼──────┐
↓ ↓ ↓
API 1 API 2 API 3It provides several benefits:
Prevents one server from receiving all requests.
We can add more instances behind the load balancer.
If one instance becomes unhealthy, traffic can be routed to healthy instances.
Advanced load balancers can route based on:
"The load balancer provides a single entry point for clients while distributing traffic across healthy backend instances."
"I'd use Layer 4 when I primarily need transport-level load balancing, while Layer 7 is useful when routing needs to understand HTTP-level information such as hostnames, paths, or headers."
This is a common interview question.
Layer 4 primarily operates using network/transport information such as:
It doesn't need to understand the HTTP request in detail.
Conceptually:
Client
↓
TCP
↓
L4 Load Balancer
↓
ServerLayer 7 understands application protocols such as HTTP/HTTPS.
It can make routing decisions based on:
URL
HTTP method
Host
Headers
CookiesFor example:
/api/products/*
↓
Product Service
/api/orders/*
↓
Order Service| Layer 4 | Layer 7 |
|---|---|
| TCP/UDP level | HTTP/application level |
| Less application awareness | Application-aware |
| Generally simpler | More routing capabilities |
| IP/port based decisions | URL/header/host etc. |
"I'd use Layer 4 when I primarily need transport-level load balancing, while Layer 7 is useful when routing needs to understand HTTP-level information such as hostnames, paths, or headers."
"Round Robin is simple sequential distribution, Weighted Round Robin accounts for different server capacities, Least Connections considers current connection load, and IP Hash maps clients to servers based on their IP."
Requests are distributed sequentially.
Request 1 → Server 1
Request 2 → Server 2
Request 3 → Server 3
Request 4 → Server 1
Request 5 → Server 2Pattern:
S1 → S2 → S3 → S1 → S2 → S3Simple and works well when servers have similar capacity and requests have reasonably similar cost.
Servers receive different amounts of traffic based on assigned weights.
Server 1 → Weight 5
Server 2 → Weight 3
Server 3 → Weight 2Server 1 receives more traffic.
Useful when:
Server 1 = 16 CPU
Server 2 = 8 CPU
Server 3 = 4 CPUThe load balancer sends the next request to the server with the fewest active connections.
Server 1 → 100 connections
Server 2 → 50 connections
Server 3 → 20 connections
New request
↓
Server 3This can be useful when requests have varying processing times.
The client's IP is used to determine the backend server.
Conceptually:
Client IP
↓
Hash
↓
ServerThis can provide a degree of client affinity.
However, it shouldn't be treated as a perfect session-management mechanism because clients can share IPs, change networks, or sit behind proxies/NAT.
"Round Robin is simple sequential distribution, Weighted Round Robin accounts for different server capacities, Least Connections considers current connection load, and IP Hash maps clients to servers based on their IP."
A health check is a mechanism used to determine whether an application instance or dependency is healthy and capable of serving traffic.
A health check is a mechanism used to determine whether an application instance or dependency is healthy and capable of serving traffic.
For example:
GET /healthHealthy response:
200 OKIf an instance repeatedly returns errors or times out:
API 1 → Healthy
API 2 → Unhealthy
API 3 → Healthythe load balancer can stop routing new traffic to API 2.
Load Balancer
/ \
↓ ↓
API 1 API 3
✅ ✅
API 2
❌
removed from
trafficbuilder.Services.AddHealthChecks();
app.MapHealthChecks("/health");You can also add checks for dependencies such as databases.
Liveness:
Is the application process alive?
Readiness:
Is the application ready to receive traffic?
For example, an application might be running but unable to connect to its required database. It may be alive but not ready to serve certain requests.
"I'd use health checks together with load balancing so unhealthy instances can be removed from traffic automatically."
A stateless service doesn't require a particular server's local state to process a request, whereas a stateful service depends on stored state associated with the service or instance.
A stateless service doesn't depend on information stored only in the memory of a particular application instance to process the next request.
Example:
Request 1
↓
API 1
Request 2
↓
API 2Both servers can process the user's requests.
Shared state, when necessary, can be stored externally:
API 1 ──┐
API 2 ──┼──→ Redis / Database
API 3 ──┘A stateful service maintains important state locally or is otherwise tied to a particular instance.
For example:
User
↓
Server 1
↓
Session stored in Server 1 memoryIf the next request goes to Server 2:
User
↓
Server 2
❌ Session isn't available locallyA stateless service doesn't require a particular server's local state to process a request, whereas a stateful service depends on stored state associated with the service or instance.
Stateless services are easier to scale horizontally because any instance can handle any request without depending on session data stored in another instance's local memory.
This is a very important interview question.
Stateless services are easier to scale horizontally because any instance can handle any request without depending on session data stored in another instance's local memory.
Suppose we have:
Load Balancer
/ | \
↓ ↓ ↓
API 1 API 2 API 3If the APIs are stateless:
Request A → API 1
Request B → API 3
Request C → API 2
Request D → API 1No problem.
If traffic increases:
Load Balancer
/ / | \ \
↓ ↓ ↓ ↓ ↓
API1 API2 API3 API4 API5we can simply add instances.
A common architecture is:
Client
↓
Load Balancer
↓
Multiple ASP.NET Core instances
↓
Redis / Database / Message QueueAuthentication using bearer tokens can also support this model because the API instances don't need to keep the user's primary authentication session in local memory.
"Statelessness removes server affinity as a requirement, which makes horizontal scaling and failover much easier."
"Sticky sessions can be useful for legacy stateful applications, but for a modern horizontally scalable API, I'd generally prefer stateless services with shared external state."
Sticky sessions, also called session affinity, mean that the load balancer tries to send subsequent requests from the same client to the same backend instance.
Example:
User A
↓
Load Balancer
↓
API 1Future requests:
User A → API 1
User A → API 1
User A → API 1They're useful when an application maintains session state locally on the server.
For example:
API 1
└── User sessionWithout affinity:
Request 1 → API 1
Request 2 → API 2
API 2 doesn't have API 1's local session1. Uneven load
One server may receive more users than others.
2. Reduced scalability
Scaling becomes less flexible.
3. Failure impact
If the server containing a user's session fails, that session may be lost unless replicated elsewhere.
4. Poorer elasticity
Dynamic addition/removal of instances becomes more complicated.
Instead of:
Sticky sessions
↓
Local server memorywe often prefer:
Stateless APIs
↓
Shared external state
↓
Redis / Database"Sticky sessions can be useful for legacy stateful applications, but for a modern horizontally scalable API, I'd generally prefer stateless services with shared external state."
A Single Point of Failure, or SPOF, is a component whose failure can cause the entire system or a critical part of the system to become unavailable.
A Single Point of Failure, or SPOF, is a component whose failure can cause the entire system or a critical part of the system to become unavailable.
Example:
Users
↓
Single API Server
↓
DatabaseIf that API server fails:
API Server ❌
Entire application unavailableThat's a SPOF.
Use multiple instances:
Load Balancer
/ | \
↓ ↓ ↓
API 1 API 2 API 3If API 2 fails:
API 1 ✅
API 2 ❌
API 3 ✅The other instances can continue serving requests.
What if we have:
Users
↓
Single Load Balancer
↓
Multiple APIsThe load balancer itself can become a SPOF.
Therefore production architecture needs redundancy at critical infrastructure layers as appropriate.
Similarly, consider the database:
Multiple APIs
↓
Single DatabaseIf the database fails, the entire application may still fail.
We might introduce:
Primary
│
├── Replica 1
└── Replica 2and appropriate failover/recovery mechanisms.
You don't simply eliminate SPOFs by adding servers everywhere.
You need to identify:
Application
Load Balancer
Database
Cache
Message Broker
Network
Storageand determine which failures the system must tolerate.
"I'd identify critical components that can bring down the system, then introduce redundancy, health checks, failover mechanisms, replication and appropriate recovery strategies. I'd also consider whether the added complexity is justified by the availability requirement."
That last sentence demonstrates trade-off thinking.
Suppose the interviewer says:
"Your ASP.NET Core API is currently deployed on one Azure VM. Traffic has increased significantly. How would you redesign it?"
A strong answer would be:
"First, I'd determine the current bottleneck using metrics such as CPU, memory, request latency, throughput and database utilization. If the application tier is the bottleneck, I'd make the API stateless and deploy multiple instances behind a load balancer or appropriate Azure application-routing service. I'd configure health checks so unhealthy instances are removed from traffic and enable autoscaling based on demand. I'd move any instance-local session state to a shared store such as Redis or the database. I'd also check whether the database or other dependencies become the next bottleneck, because scaling the API layer alone won't solve a database bottleneck."
That's the kind of answer you should aim for.
Memorize this architecture:
INTERNET
│
▼
┌─────────────┐
│Load Balancer│
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
API 1 API 2 API 3
│ │ │
└────────────┼────────────┘
│
┌──────┴──────┐
▼ ▼
Redis DatabaseAnd these concepts:
| Concept | Remember |
|---|---|
| Vertical scaling | Bigger machine |
| Horizontal scaling | More machines |
| Load balancer | Distributes traffic |
| Round Robin | Sequential distribution |
| Least Connections | Fewest active connections |
| Health Check | Detect unhealthy instances |
| Stateless | Any instance can handle request |
| Sticky Session | Same client → same instance |
| SPOF | One failure can take down critical system |
| Redundancy | Multiple instances/components |
Don't think:
"More servers = scalable system."
Think:
"Can I add/remove instances safely, distribute traffic, survive instance failures, and avoid relying on local state?"
That's the foundation of horizontal scalability.
A database can become a bottleneck when its available resources or processing capacity are insufficient to handle the workload generated by application servers.
A database can become a bottleneck when its available resources or processing capacity are insufficient to handle the workload generated by application servers.
Even if we add multiple ASP.NET Core instances, they may all send queries to the same database. This increases database CPU utilization, memory pressure, disk I/O, lock contention, and connection usage.
For example:
Load Balancer
/ | \
▼ ▼ ▼
API 1 API 2 API 3
\ | /
\ | /
▼ ▼ ▼
SQL Server
CPU: 100%Adding more API instances won't necessarily improve performance because all instances depend on the same overloaded database.
I'd first identify the actual bottleneck using database metrics and query execution plans. Depending on the findings, I would:
Key interview point: Always identify the bottleneck before selecting a scaling strategy.
SQL databases are generally relational databases that organize data into tables with defined schemas, relationships, and constraints.
SQL databases are generally relational databases that organize data into tables with defined schemas, relationships, and constraints. They support SQL queries and commonly provide transactions with ACID guarantees.
NoSQL is a broad category that includes document, key-value, wide-column, and graph databases. These systems offer different data models and consistency, transaction, and scaling capabilities.
| Feature | SQL | NoSQL |
|---|---|---|
| Data model | Relational tables | Document, key-value, wide-column, graph, etc. |
| Schema | Typically structured | Often flexible or model-specific |
| Relationships | Foreign keys and joins | Often uses embedded or denormalized data |
| Transactions | Strong transactional support | Depends on the database |
| Scaling | Vertical and horizontal options | Many systems are designed for distributed scaling |
| Best fit | Relational data and transactional workloads | Workloads suited to specific non-relational data models |
For an order-management system:
Customers
│
└── Orders
│
└── OrderItemsA relational database is a natural choice because these entities have relationships and transactional requirements.
For a distributed session store:
sessionId → sessionDataA key-value database such as Redis could be appropriate.
No. Performance depends on the database implementation, data model, query patterns, workload, consistency requirements, and configuration.
Remember: Choose the database based on requirements and access patterns, not simply because one category is supposedly faster.
0;
I'd choose SQL Server when the application has relational data, complex queries, transactional requirements, constraints, and a need for reliable consistency across related records.
For example, consider an order-processing application involving customers, orders, inventory, and payments.
A transaction may need to update multiple related records together. SQL Server supports ACID transactions and relational constraints that help maintain data integrity.
Example:
BEGIN TRANSACTION;
UPDATE Inventory
SET Quantity = Quantity - 1
WHERE ProductId = 100
AND Quantity > 0;
-- Application must verify that exactly one row was updated.
-- Otherwise, roll back.
COMMIT TRANSACTION;In a real implementation, the application must check the affected row count and roll back if the update did not succeed. Payment processing may also require idempotency and coordination with external services.
"I wouldn't choose SQL or NoSQL based on popularity. I'd evaluate the data relationships, query patterns, transaction boundaries, consistency needs, throughput, and operational cost. For a relational order-processing system, SQL Server would often be my starting point."
A database index is a data structure that helps the database locate records efficiently without scanning every row in a table.
A database index is a data structure that helps the database locate records efficiently without scanning every row in a table.
Suppose we have a table containing millions of users:
SELECT *
FROM Users
WHERE Email = 'user@example.com';Without a suitable index, SQL Server may need to scan many rows to find the matching email.
With an appropriate index:
CREATE INDEX IX_Users_Email
ON Users(Email);SQL Server can use the index to locate matching records more efficiently, depending on the query plan and selectivity.
Conceptually:
Query: Find user by email
Index on Email
Locate the matching key
Matching row(s)
No. Indexes consume storage and add overhead to inserts, updates, and deletes. I'd create indexes based on frequently executed queries, filtering, joins, sorting, and measured execution plans.
Indexes improve certain read operations but introduce storage and maintenance costs.
Indexes improve certain read operations but introduce storage and maintenance costs.
Every time an indexed column changes, the database may need to update the corresponding index structure.
Too many indexes can cause:
Suppose an orders table receives thousands of inserts per second.
Orders table
├── Index on CustomerId
├── Index on OrderDate
├── Index on Status
├── Index on Amount
└── Index on another columnIf several indexes aren't useful to actual queries, they may add overhead without providing meaningful benefits.
I'd examine:
Interview takeaway: Indexing is a trade-off between read performance, write performance, and storage cost.
Database replication involves maintaining copies of data on multiple database instances or nodes.
Database replication involves maintaining copies of data on multiple database instances or nodes.
A common architecture has a primary database that accepts writes and one or more replicas that receive changes.
Primary Database
Accepts writes
Replicate changes
Replica 1
Replica 2
Replication can provide:
Replication does not automatically guarantee zero data loss or automatic failover. Those depend on the replication mode, failure-detection mechanism, configuration, and recovery strategy.
Not necessarily. In a primary-replica setup, writes generally still go through the primary, which must also handle replication work. Replication primarily helps with redundancy and, when appropriately configured, read scaling.
"I'd introduce read replicas when read traffic is a bottleneck, but I'd route reads according to their consistency requirements rather than sending every read to a replica."
A read replica is a copy of a database that can serve read queries while the primary database typically handles writes.
It's particularly useful when an application has significantly more reads than writes.
For example:
Application
/ \
Writes Reads
│ │
▼ ▼
Primary Read Replicas
/ \
▼ ▼
Replica 1 Replica 2Suppose the workload is:
After verifying the primary's actual capacity, we could distribute suitable read traffic across replicas to reduce the primary's read workload.
Good candidates may include:
Suppose a user updates their address and immediately requests it again. If that read goes to a lagging replica, it might return the old address.
For read-after-write consistency, the application may need to read from the primary or use a database-specific consistency mechanism.
"I'd introduce read replicas when read traffic is a bottleneck, but I'd route reads according to their consistency requirements rather than sending every read to a replica."
Replication lag is the delay between a change being committed on the primary database and that change becoming visible on a replica.
Replication lag is the delay between a change being committed on the primary database and that change becoming visible on a replica.
Consider this sequence:
T1: User updates profile
│
▼
Primary: Name = "Ramesh"
│
│ Replication delay
▼
Replica: Name = old valueIf the next request reads from the replica before the change arrives, the user may see stale data.
Important: Replication lag is not necessarily a failure. It becomes a problem when the application's consistency expectations are stricter than the replica's guarantees.
"Replication addresses the need for additional copies of data, while sharding distributes different portions of the data across nodes. They're complementary techniques, not alternatives."
Replication creates additional copies of data, whereas sharding divides data across multiple database partitions or nodes.
All data
Copy A
Copy B
Purpose: redundancy, availability and potentially read scaling.
All data
Shard A
Part 1
Shard B
Part 2
Purpose: distribute data, storage and workload.
Suppose we have 100 million customers.
With replication, multiple database nodes may hold the same 100 million customers.
With sharding:
Shard 1 → Customers A–F
Shard 2 → Customers G–M
Shard 3 → Customers N–ZEach shard stores a different subset of customers.
Yes. A common architecture distributes data across shards and replicates each shard for availability or read scaling.
"Replication addresses the need for additional copies of data, while sharding distributes different portions of the data across nodes. They're complementary techniques, not alternatives."
"I'd select a shard key based on data distribution, traffic distribution, and the application's most common queries. I'd also evaluate hotspot risk and the operational complexity of resharding before committing to the design."
A shard key is the field, or combination of fields, used to determine which shard stores a particular record.
For example, in a customer database, we might use CustomerId to route records to shards.
Conceptually:
CustomerId
│
▼
Shard Routing Logic
│
├── Shard 1
├── Shard 2
└── Shard 3The application or database routing layer uses the shard key to locate the relevant shard.
1\. Even data distribution
Each shard should receive a reasonable share of the data.
2\. Even traffic distribution
One shard shouldn't receive a disproportionate share of requests.
3\. Query compatibility
The shard key should support common query patterns and avoid unnecessary cross-shard operations.
4\. Stability
The key should not change frequently, because moving records between shards can be expensive.
5\. Sufficient cardinality
There should be enough distinct key values to distribute the workload effectively.
Suppose the user population is:
India → 80%
US → 10%
UK → 5%
Others → 5%Sharding only by country may overload the India shard while leaving other shards underutilized.
A hash function can distribute customer IDs across shards:
shard = hash(CustomerId) % numberOfShardsThis is a simplified illustration. In real systems, changing the number of shards can cause many records to be remapped; consistent hashing or other routing strategies can help, depending on the database.
"I'd select a shard key based on data distribution, traffic distribution, and the application's most common queries. I'd also evaluate hotspot risk and the operational complexity of resharding before committing to the design."
Interviewer: Your ASP.NET Core e-commerce API is experiencing high SQL Server CPU utilization. How would you improve the architecture?
A good answer should be progressive:
1\. Measure the bottleneck
CPU, I/O, waits, connections, query plans
2\. Optimize SQL
Queries, indexes, execution plans, N+1 calls
3\. Reduce database workload
Caching, fewer round trips, pagination
4\. Scale the right workload
Read replicas, partitioning, suitable database scaling
5\. Consider sharding if necessary
When one database can no longer meet capacity requirements
"I'd first examine query execution plans, database waits, CPU utilization, I/O, and connection usage to identify the actual cause. I'd optimize expensive queries and indexes, remove unnecessary database calls, and introduce caching for suitable frequently accessed data. If reads are the main bottleneck, I'd evaluate read replicas while accounting for replication lag. If storage or write throughput exceeds what a single database can support, I'd consider partitioning or sharding. I'd choose the solution based on measured bottlenecks and consistency requirements rather than jumping straight to sharding."
| Concept | Remember |
|---|---|
| SQL | Relational data, joins, constraints, transactions |
| NoSQL | Multiple non-relational models for specific access patterns |
| Indexing | Faster suitable reads; additional write and storage costs |
| Replication | Maintain copies of data |
| Read replica | Offload suitable read traffic |
| Replication lag | Replica may temporarily contain stale data |
| Sharding | Distribute different data subsets across nodes |
| Shard key | Determines where records are stored |
| Hotspot | One shard receives disproportionate traffic |
| Database bottleneck | Diagnose before adding complexity |
0/5 answered
1\. Which technique creates additional copies of database data?
Sharding
Replication
Partition pruning
Indexing
2\. Which issue can cause a read replica to return an old value?
Horizontal scaling
Index fragmentation only
Replication lag
Load balancing
3\. What should you do first when SQL Server CPU reaches 100%?
Immediately shard the database
Measure and identify the bottleneck
Add API servers only
Replace SQL with NoSQL
4\. What is the main role of a shard key?
Encrypt database records
Determine which shard stores a record
Replicate every write
Create an index automatically
5\. Why can excessive indexes hurt performance?
They prevent all SELECT queries
They eliminate transactions
They add storage and write-maintenance costs
They always cause stale reads
Caching is a technique that temporarily stores frequently accessed or expensive-to-compute data in a faster storage layer so that subsequent requests can retrieve it without repeating the original work.
Caching is a technique that temporarily stores frequently accessed or expensive-to-compute data in a faster storage layer so that subsequent requests can retrieve it without repeating the original work.
For example, suppose an ASP.NET Core API retrieves product details from SQL Server. If thousands of users request the same product, querying SQL Server for every request creates unnecessary database load.
We can cache the product in Redis.
Client
ASP.NET Core API
Redis Cache
Hit: return cached data
Miss: query SQL Server
Assume, illustratively, a database lookup takes 50 ms and a cache lookup takes 2 ms.
For repeated requests, a cache hit may avoid most of that database lookup time. Actual end-to-end latency depends on networking, serialization, application processing, and other factors.
Important: Caching doesn't automatically make every request faster. Cache misses, serialization, network calls, and invalidation overhead must also be considered.
Follow-up: Can caching replace a database?
No. In a typical application, the database remains the authoritative source of durable business data, while the cache stores temporary copies that can be rebuilt.
Redis is an in-memory data store that supports low-latency access to data structures and is commonly used for caching, session storage, counters, and rate limiting.
Redis is an in-memory data store that supports low-latency access to data structures and is commonly used for caching, session storage, counters, and rate limiting.
For example, a product could be cached with the key:
Key: product:100
Value: {"id":100,"name":"Keyboard","price":1500}
TTL: 300 secondsThe key identifies the cached value, and the TTL specifies its expiration time.
| Redis | SQL Server |
|---|---|
| Commonly used for low-latency cache access | Relational database |
| Key-based access and data structures | Rich SQL queries and joins |
| TTL-based expiration is common | Durable business records |
| Persistence is configurable | Designed for durable relational storage |
Redis is optimized for low-latency data access, but real performance depends on network latency, data size, workload, configuration and database query efficiency.
Interview takeaway: Redis complements SQL Server; it doesn't automatically replace it.
IMemoryCache stores cached objects in the memory of the application process.
IMemoryCache stores cached objects in the memory of the application process. IDistributedCache provides an abstraction for caching through an external or distributed cache provider, such as Redis.
builder.Services.AddMemoryCache();Example:
_cache.Set(
"product:100",
product,
TimeSpan.FromMinutes(5));The data is local to that application instance.
Register a distributed cache provider, then use methods such as:
await cache.SetStringAsync(
"product:100",
json,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(5)
});| Feature | IMemoryCache | IDistributedCache |
|---|---|---|
| Storage | Local process memory | External/shared cache provider |
| Multiple API instances | Separate caches | Can share cached data |
| Access overhead | Very low | Usually includes network overhead |
| Data type | Can store objects directly | Interface exposes byte/string operations |
| Typical use | Local, instance-specific cache | Shared cache across instances |
Suppose your application runs on three servers.
With IMemoryCache:
API 1 → Cache A
API 2 → Cache B
API 3 → Cache CThese caches can contain different values.
With Redis-backed IDistributedCache:
API 1 ──┐
API 2 ──┼── Redis
API 3 ──┘All three instances can access the same cached entries.
Important: IDistributedCache is only an abstraction. Registering AddDistributedMemoryCache() does not create a shared Redis cache; it uses in-process memory.
GetProductAsync(int id)
Cache-aside, also called lazy loading, is a pattern where the application checks the cache first. If the requested value isn't present, it loads the data from the database, stores it in the cache, and returns it.
1\. Request product 100
2\. Check Redis
Hit
Return cached value
Miss
Query SQL Server
3\. Cache database result with TTL
4\. Return product
public async Task<Product?> GetProductAsync(int id)
{
string key = $"product:{id}";
var cached = await _cache.GetStringAsync(key);
if (cached is not null)
{
return JsonSerializer.Deserialize<Product>(cached);
}
var product = await _dbContext.Products
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == id);
if (product is not null)
{
await _cache.SetStringAsync(
key,
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(5)
});
}
return product;
}Here, _cache is an IDistributedCache, and _dbContext is an EF Core DbContext. The required namespaces and dependency registrations must be configured.
Follow-up: What happens if Redis is unavailable?
The application needs an explicit failure strategy. For noncritical cache reads, it may fall back to SQL Server, subject to database capacity and timeout limits. For cache writes, it may log the error and continue if the database update has already succeeded. Don't allow a cache outage to trigger unlimited database traffic.
These patterns describe how cache data and authoritative storage are updated.
These patterns describe how cache data and authoritative storage are updated.
The application loads data into the cache when a read misses.
Read → Cache miss → Database → CacheBest for: Read-heavy workloads with frequently reused data.
Trade-off: Cache misses add latency, and writes require a separate invalidation/update strategy.
The write path updates the authoritative database and cache as part of the chosen write strategy.
Write → Database + Cache updateBest for: Workloads where keeping the cache fresh after successful writes is valuable.
Trade-off: More coordination and error handling are required. SQL Server and Redis do not automatically share one atomic transaction.
The cache accepts changes first, and the database is updated asynchronously later.
Write → Cache → Async persistence → DatabaseBest for: Carefully designed workloads such as aggregations and buffered events.
Trade-off: Changes may be lost before persistence unless the system has appropriate durability, retry and recovery mechanisms.
I'd usually start with cache-aside and invalidate the product cache after a successful database update.
It is straightforward and separates authoritative writes from cache population.
For payments, balances or inventory correctness, I would not rely on a cache write as proof that the business transaction has been durably committed.
TTL, or Time To Live, specifies how long a cache entry remains valid before expiration.
TTL, or Time To Live, specifies how long a cache entry remains valid before expiration.
For example:
Key: product:100
Value: Keyboard
TTL: 300 secondsThe entry expires after five minutes, unless its expiration is changed.
Consider:
| Data | Example strategy |
|---|---|
| Product catalogue | Moderate TTL |
| Inventory | Short TTL or explicit invalidation |
| Static reference data | Longer TTL |
| Frequently changing user profile | TTL plus invalidation |
| Critical payment state | Authoritative reads and correctness controls |
These are starting points, not fixed rules.
For example, with a 10-minute sliding expiration, a frequently accessed entry may remain cached as long as it continues to be accessed within each 10-minute window. It can still expire according to other configured limits.
Interview takeaway: TTL balances freshness, memory usage, and cache-hit rate.
"I'd treat SQL Server as the source of truth. After a successful database update, I'd invalidate the corresponding Redis key. For stronger reliability, I'd consider a transactional outbox and background processing so cache invalidation can be retried if Redis is unavailable."
Cache invalidation is the process of removing or refreshing cached data when the cached value is no longer valid.
Suppose Redis stores:
product:100 → price = ₹1,500The product price changes in SQL Server to ₹1,700. If Redis isn't updated or invalidated, the application may continue returning the old price.
1. Update product in SQL Server
2. Commit the database transaction
3. Delete product:100 from Redis
4. Next read loads the updated valueExample:
await _dbContext.SaveChangesAsync();
await _cache.RemoveAsync($"product:{product.Id}");This snippet assumes the database update has succeeded. It also assumes cache invalidation is best-effort; production code should handle failures and consider retry or reconciliation mechanisms.
SQL Server may contain the new value while Redis still holds the old one.
Possible mitigations include:
An outbox can help prevent the invalidation event from being lost when the database transaction commits, but consumers still need retries and idempotent processing.
"I'd treat SQL Server as the source of truth. After a successful database update, I'd invalidate the corresponding Redis key. For stronger reliability, I'd consider a transactional outbox and background processing so cache invalidation can be retried if Redis is unavailable."
These are three different problems that can cause cache effectiveness to degrade or database load to spike.
These are three different problems that can cause cache effectiveness to degrade or database load to spike.
Many requests try to load the same missing or expired cache entry simultaneously.
Example: A popular product expires from Redis, and thousands of requests query SQL Server for that product at once.
Solutions: Request coalescing, single-flight loading, suitable locking and TTL jitter.
Requests repeatedly ask for records that don't exist, so they keep missing the cache and reaching the database.
Example: Thousands of requests for a nonexistent product ID.
Solutions: Validate requests, rate-limit abusive traffic and use short-lived negative caching when appropriate.
Many cache entries expire or become unavailable around the same time, causing a surge of requests to the database.
Solutions: TTL jitter, staggered expiration, controlled cache warming and database capacity planning.
{
I'd use a Redis-backed distributed cache so multiple ASP.NET Core instances can access the same cache.
The architecture would look like this:
Load Balancer
API 1
API 2
API 3
Redis — shared cache
SQL Server — source of truth
IDistributedCache provider.product:100.For a Redis-backed provider such as Microsoft.Extensions.Caching.StackExchangeRedis, configuration can be registered using the package's supported extension methods and your deployment's Redis connection settings.
Conceptually:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp:";
});This requires the appropriate provider package and a valid Redis connection string. Keep credentials in secure configuration, not in source control.
IMemoryCache?With multiple instances, each local cache can contain different values. Redis gives the instances a shared cache, although invalidation and consistency still need to be designed correctly.
"I'd measure Redis hit rates and latency alongside SQL Server query performance. If caching isn't working as expected, I'd investigate key design, TTLs, invalidation and stampedes. I'd optimize expensive queries and cache suitable read-heavy data. If writes are the bottleneck, I'd optimize the write path rather than assuming Redis will solve it. I'd also design controlled fallback behavior so a Redis outage doesn't overwhelm SQL Server."
I'd first investigate why database requests remain high despite the cache.
Potential causes include:
I'd examine:
Redis:
- Hit/miss ratio
- Latency
- Evictions
- Errors
SQL Server:
- CPU and I/O
- Expensive queries
- Wait statistics
- Connection usage
- Query execution plans
Application:
- Request volume
- Endpoint latency
- Cache errors
- Database calls per requestIf a frequently requested product is repeatedly missing from Redis, I would inspect key generation, TTLs and invalidation behavior.
If SQL Server is overloaded by writes, read caching may not solve the main problem.
| Cause | Potential solution |
|---|---|
| Low cache-hit rate | Review key design, TTL and caching eligibility |
| Expensive reads | Optimize queries and indexes |
| Cache stampede | Request coalescing, single-flight and TTL jitter |
| Heavy read traffic | Cache suitable data or add read replicas |
| Heavy write traffic | Optimize write paths and transactions; consider partitioning or other scaling strategies if justified |
| Redis outages | Improve monitoring, timeouts and controlled fallback behavior |
"I'd measure Redis hit rates and latency alongside SQL Server query performance. If caching isn't working as expected, I'd investigate key design, TTLs, invalidation and stampedes. I'd optimize expensive queries and cache suitable read-heavy data. If writes are the bottleneck, I'd optimize the write path rather than assuming Redis will solve it. I'd also design controlled fallback behavior so a Redis outage doesn't overwhelm SQL Server."
| Concept | One-line answer |
|---|---|
| Caching | Stores reusable data for faster access |
| Redis | In-memory data store commonly used as a shared cache |
IMemoryCache | Local, per-process cache |
IDistributedCache | Abstraction for shared/distributed cache providers |
| Cache-aside | Read cache first; load from database on miss |
| Write-through | Update authoritative storage and cache as part of the write strategy |
| Write-behind | Persist cached changes asynchronously |
| TTL | Expiration time for cached data |
| Cache invalidation | Remove or refresh stale cached entries |
| Stampede | Many requests load the same missing key |
| Penetration | Repeated requests for nonexistent keys |
| Avalanche | Many entries expire or disappear together |
Next: Day 5 — Message Queues and Asynchronous Processing. We'll cover RabbitMQ, Azure Service Bus, Kafka, producer/consumer architecture, decoupling, retries, dead-letter queues, idempotency and how to use background processing in .NET.
A message queue is a communication mechanism that allows one application or service to send messages to another service without requiring the receiver to process them immediately.
A message queue is a communication mechanism that allows one application or service to send messages to another service without requiring the receiver to process them immediately.
It acts as an intermediary between a producer and a consumer.
Producer
Order API publishes an order
Message Broker / Queue
Stores the message until it can be processed
Consumer
Inventory or notification service
When a customer places an order:
OrderPlaced message.The customer does not have to wait for every downstream task to finish.
Interview answer: “A message queue decouples services and enables asynchronous processing, buffering, retries, and independent scaling. It improves resilience and helps systems handle traffic spikes.”
Important: A queue does not automatically guarantee that messages will never be lost. Reliability depends on durable messaging, publisher confirmation, acknowledgment strategy, retries, and correct application design.
### Synchronous example...
| Aspect | Synchronous | Asynchronous |
|---|---|---|
| Communication | Caller waits for a response | Caller can continue without waiting for the final result |
| Common example | HTTP request-response | Queue or event-driven processing |
| Coupling | Often tighter at runtime | Usually looser |
| User response | May be delayed by downstream work | Can return quickly after accepting the request |
| Failure handling | Downstream failure can affect caller immediately | Work can be retried later if reliably stored |
| Complexity | Often simpler | Requires handling retries, duplicates, and eventual consistency |
An API calls a payment service and waits for the result.
Client → Order API → Payment API
↓
Client ← Order API ← Payment resultIf the payment service takes 8 seconds, the Order API may remain waiting during that time.
Client → Order API → Save order + publish message
Client ← Order accepted
Queue → Payment Worker
→ Notification WorkerThe API can respond with 202 Accepted when processing has been accepted for later completion, or return 201 Created if the order resource has already been created. The correct status depends on the API contract.
Use synchronous communication when the caller needs an immediate result—for example, retrieving a product or validating a login.
Use asynchronous communication when work can finish later—for example, sending emails, generating reports, processing images, or running long workflows.
Interview tip: async and await in C# do not automatically make communication asynchronous in the distributed-systems sense. An async HTTP call still waits for the remote response. Queue-based processing lets the request and the actual work have separate lifecycles.
Both are messaging patterns, but they distribute messages differently.
Both are messaging patterns, but they distribute messages differently.
┌→ Worker A
Producer → Queue ───┤
└→ Worker BIf Worker A and Worker B compete for messages, a particular message is normally delivered to one of them for processing. Redelivery may happen if processing fails or acknowledgment is missing.
Use cases: Order fulfillment, background jobs, image processing, report generation.
┌→ Inventory Service
Producer → Topic/Event ├→ Notification Service
└→ Analytics ServiceEach subscriber or subscription receives its own copy or logical delivery of the event, depending on the broker's model and configuration.
Use cases: An order-created event consumed by inventory, notifications, analytics, and shipping.
| Feature | Queue | Pub/Sub |
|---|---|---|
| Primary goal | Distribute work | Broadcast events |
| Message handling | Usually one competing consumer per delivery | Multiple subscriptions can independently receive the event |
| Common pattern | Work queue | Topic with subscriptions |
| Example | Process a report once | Notify several systems that a report was created |
Interview answer: “A queue distributes work among consumers, while Pub/Sub allows multiple independent subscribers to react to the same event. The exact delivery behavior depends on the broker and subscription configuration.”
All three support messaging, but they are optimized for different needs.
All three support messaging, but they are optimized for different needs.
RabbitMQ Logo & Brand Assets (SVG, PNG and vector) - Brandfetch
RabbitMQ
Apache Kafka
Azure Service Bus
| Feature | RabbitMQ | Kafka | Azure Service Bus |
|---|---|---|---|
| Primary model | Message broker | Event log / streaming | Managed enterprise messaging |
| Message replay | Depends on queue and retention setup | Core capability within retention | Not designed as a general-purpose replayable event log |
| Routing | Exchanges and bindings | Topics and partitions | Queues, topics, subscriptions, rules |
| Typical use | Background tasks | High-volume event streams | Azure business workflows |
| Operations | Self-managed or managed options | Self-managed or managed options | Azure-managed service |
For a .NET application already hosted in Azure, Azure Service Bus is often a practical starting point. But choose based on throughput, ordering, replay requirements, cost, operational needs, and the team's experience—not simply because the application uses Azure.
These terms describe how a messaging system handles delivery and redelivery.
These terms describe how a messaging system handles delivery and redelivery.
| Delivery guarantee | Meaning | Main risk |
|---|---|---|
| At-most-once | A message is delivered zero or one time | A message can be lost |
| At-least-once | A message is delivered one or more times | Duplicate processing |
| Exactly-once | A defined processing effect occurs exactly once within a supported scope | Difficult to guarantee across multiple independent systems |
A message may be lost if the consumer fails after receiving it but before completing the operation.
Receive message → Acknowledge → Process
↓
Process failsThis approach can be suitable when occasional loss is acceptable.
The consumer acknowledges the message only after successful processing.
Receive → Process → Save result → AcknowledgeIf the consumer crashes after saving the result but before acknowledging, the broker may redeliver the message. The consumer could therefore process it again.
For important business workflows, at-least-once delivery combined with idempotent processing is a common design.
A broker may provide exactly-once guarantees within a particular scope, such as specific stream-processing operations or transactional messaging features. However, that does not automatically guarantee exactly one external effect across a database, payment provider, and email service.
For example, a payment may succeed but the consumer may crash before recording the success. Retrying blindly could charge the customer twice.
Use techniques such as:
Interview answer: “I usually design consumers for at-least-once delivery and make their business operations idempotent. Exactly-once behavior across distributed systems requires a clearly defined scope and additional coordination; I don't assume the broker alone guarantees it.”
x.MessageId == message.MessageId,
An operation is idempotent if repeating the same operation produces the same final effect as performing it once.
For a message consumer, idempotency prevents a redelivered message from causing duplicate business effects.
Suppose an OrderPlaced message is delivered twice. The consumer should not create two inventory reservations for the same order.
A simple database-backed pattern uses a unique message ID:
CREATE TABLE ProcessedMessages
(
MessageId UNIQUEIDENTIFIER PRIMARY KEY,
ProcessedAt DATETIME2 NOT NULL
);Conceptually, the consumer does the following:
ProcessedMessages.The unique constraint handles concurrent attempts to process the same message. The implementation must catch the duplicate-key case and treat it as an already-processed message.
public async Task HandleAsync(
OrderPlaced message,
CancellationToken cancellationToken)
{
await using var transaction =
await db.Database.BeginTransactionAsync(
cancellationToken);
var alreadyProcessed = await db.ProcessedMessages
.AnyAsync(x => x.MessageId == message.MessageId,
cancellationToken);
if (alreadyProcessed)
return;
db.ProcessedMessages.Add(new ProcessedMessage
{
MessageId = message.MessageId,
ProcessedAt = DateTime.UtcNow
});
var order = await db.Orders.FindAsync(
new object[] { message.OrderId },
cancellationToken);
if (order is null)
throw new InvalidOperationException("Order not found.");
order.Status = "Processing";
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}This illustrates the pattern, but production code must handle concurrent duplicates and ensure the transaction's behavior matches the database and broker acknowledgment strategy. A unique constraint remains essential even if the code checks for an existing message first.
Important: This protects database changes in the same transaction. It does not automatically make an external payment API or email send idempotent. For external effects, use provider-supported idempotency keys or a separate reliable workflow.
Interview answer: “Idempotency is essential because messages can be redelivered. I use stable message IDs, database constraints, and atomic transactions to ensure duplicate delivery doesn't repeat the business effect.”
These mechanisms help a messaging system recover from failures without endlessly blocking processing.
These mechanisms help a messaging system recover from failures without endlessly blocking processing.
A retry attempts to process a failed message again.
Typical policy:
For example, retry after approximately 1, 2, 4, and 8 seconds, subject to a maximum delay and a configured attempt limit.
A DLQ stores messages that cannot be processed successfully under the configured delivery and failure policy.
It allows engineers to inspect failures, identify the cause, and replay or correct messages deliberately.
Common reasons include:
A poison message repeatedly causes processing to fail—for example, a message with invalid JSON that crashes the consumer on every attempt.
Queue
Consumer attempts processing
Transient failure?
Yes
Retry with backoff
No / limit reached
Dead-letter queue
Interview answer: “I use bounded retries with exponential backoff for transient failures, then dead-letter messages that cannot be processed. I monitor the DLQ and investigate before replaying messages to avoid duplicates or repeated failures.”
The transactional outbox pattern solves a common distributed-systems problem: saving data in a database and publishing a message are two separate operations.
The transactional outbox pattern solves a common distributed-systems problem: saving data in a database and publishing a message are two separate operations.
Imagine an Order API:
OrderPlaced message to a broker.Two failure cases are possible:
A normal SQL transaction does not atomically commit a separate broker publish.
Write the order and an outbox record in the same database transaction.
Single SQL transaction
Insert Order + Insert Outbox message
Outbox Publisher
Reads unsent records and publishes them
Message Broker
Consumers process published events
If the transaction rolls back, neither the order nor its outbox message is committed. If it succeeds, the outbox record remains available for publishing, even if the broker is temporarily unavailable.
CREATE TABLE OutboxMessages
(
Id UNIQUEIDENTIFIER PRIMARY KEY,
MessageType NVARCHAR(200) NOT NULL,
Payload NVARCHAR(MAX) NOT NULL,
CreatedAt DATETIME2 NOT NULL,
PublishedAt DATETIME2 NULL
);Inside one EF Core transaction:
await using var transaction =
await db.Database.BeginTransactionAsync(cancellationToken);
db.Orders.Add(order);
db.OutboxMessages.Add(new OutboxMessage
{
Id = Guid.NewGuid(),
MessageType = "OrderPlaced",
Payload = JsonSerializer.Serialize(orderEvent),
CreatedAt = DateTime.UtcNow
});
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);A separate background publisher then:
Interview answer: “The transactional outbox makes the database change and intent to publish atomic. A background publisher eventually delivers the event, while idempotent consumers handle possible duplicate delivery.”
_logger;
ASP.NET Core provides BackgroundService and IHostedService for running background work inside an application host.
BackgroundServicepublic sealed class OrderWorker : BackgroundService
{
private readonly ILogger<OrderWorker> _logger;
public OrderWorker(
ILogger<OrderWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation(
"Checking for pending orders");
// Fetch and process a small batch
// of pending orders from the database.
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
catch (OperationCanceledException)
when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(
ex, "Order worker iteration failed");
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}
}Register it in Program.cs:
builder.Services.AddHostedService<OrderWorker>();This demonstrates the worker lifecycle. The database polling, batch selection, concurrency handling, and processing logic still need to be implemented.
EF Core's DbContext is typically registered as a scoped service. Do not inject a scoped DbContext directly into a long-lived singleton worker.
Instead, inject IServiceScopeFactory and create a scope for each processing batch:
public sealed class OrderWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderWorker> _logger;
public OrderWorker(
IServiceScopeFactory scopeFactory,
ILogger<OrderWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var processor = scope.ServiceProvider
.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingBatchAsync(
stoppingToken);
}
catch (OperationCanceledException)
when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(
ex, "Background processing failed");
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}
}Register the dependencies:
builder.Services.AddScoped<IOrderProcessor, OrderProcessor>();
builder.Services.AddHostedService<OrderWorker>();This assumes IOrderProcessor implements the batch-processing method shown in the example.
BackgroundService enough, and when do you need a broker?| Scenario | Suitable choice |
|---|---|
| A periodic cleanup job | BackgroundService |
| Polling a small table of pending records | Background worker, with safe database coordination |
| Simple in-process work queue | Channel<T> plus a hosted worker |
| Durable work that must survive application restarts | Durable broker or persistent database-backed queue |
| Multiple independently deployed consumers | Message broker |
| Traffic spikes and buffering | Message broker |
| Dead-lettering, broker-managed retries, and routing | Message broker with appropriate configuration |
Crucial trade-off: An in-memory queue such as Channel<T> is not durable. If the process terminates, queued items can be lost. A background worker also runs within the application lifecycle, so deploying several application instances requires care to avoid duplicate processing.
For Azure-based .NET systems, Azure Service Bus with a BackgroundService consumer is a common combination: the broker handles durable messaging, while the worker hosts the processing code.
This is a common system design interview question.
This is a common system design interview question. I would start by clarifying the requirements, then design the simplest reliable architecture that meets them.
Functional requirements
Non-functional requirements
Client / Web Application
Load Balancer / API Gateway
Order API — ASP.NET Core
Validate request, persist order and outbox record
SQL Server
Orders + Outbox, committed in one transaction
Outbox Publisher → Durable Message Broker
For example, Azure Service Bus
Payment Worker
Idempotent payment processing
Inventory Worker
Reserve stock safely
Notification Worker
Send order updates
Monitoring
Logs, metrics, alerts
201 Created when the order resource has been created, even though downstream work is pending.Suppose normal traffic is 100 orders per second, but a promotion increases incoming traffic to 1,000 orders per second.
The broker can buffer messages while consumers process them at a sustainable rate.
I would:
A queue smooths bursts; it does not create unlimited processing capacity. If the incoming rate continuously exceeds the processing rate, the backlog will keep growing.
| Failure | Protection |
|---|---|
| API crashes after saving order | Database transaction preserves committed order |
| Message broker temporarily unavailable | Outbox record remains available for later publishing |
| Publisher crashes after sending | Retry publication; consumers tolerate duplicates |
| Consumer crashes before completing work | Broker redelivery, depending on acknowledgment and lock behavior |
| Consumer crashes after DB commit but before acknowledgment | Idempotent consumer prevents repeated business effects |
| Payment provider times out | Use provider-supported idempotency and reconcile uncertain outcomes |
| Message repeatedly fails | Bounded retries, dead-letter queue, alerts, and controlled recovery |
| Database becomes overloaded | Rate limiting, bounded concurrency, capacity planning, and query optimization |
I would model the order lifecycle explicitly, for example:
Pending → Processing → Confirmed
Alternative terminal outcomes could include PaymentFailed or Cancelled.
Because multiple services participate, the order workflow is a distributed process. If payment succeeds but inventory reservation fails, the system needs a defined compensation or recovery strategy—for example, refunding the payment if the business rules require it. A distributed workflow may use a Saga pattern rather than a single transaction spanning all services.
Track:
Use correlation IDs and structured logs so a single order can be traced across the API, broker, and consumers.
For a Microsoft Azure stack, one reasonable implementation is:
BackgroundService consumers hosted in a suitable worker deployment.These are implementation options, not mandatory components. The final choices depend on expected throughput, latency, budget, operational requirements, and the existing environment.
“I would build an ASP.NET Core Order API backed by SQL Server, using the transactional outbox pattern to ensure that a committed order also has a durable record of the event to publish. Azure Service Bus would buffer work during traffic spikes. Independently scalable consumers would process payment, inventory, and notifications with bounded retries, dead-letter handling, and idempotency. I would monitor queue depth, message age, consumer failures, and database health. For workflows involving multiple services, I would define explicit state transitions and compensation logic rather than relying on a distributed database transaction.”
| Concept | Remember this |
|---|---|
| Message queue | Decouples producers from consumers |
| Synchronous vs asynchronous | Immediate response vs work that can finish later |
| Queue vs Pub/Sub | Distribute work vs broadcast events |
| RabbitMQ | Flexible message routing and queues |
| Kafka | Durable, replayable event streams |
| Azure Service Bus | Managed enterprise messaging on Azure |
| At-least-once | Redelivery is possible; make consumers idempotent |
| Idempotency | Duplicate processing does not duplicate the business effect |
| DLQ | Isolates messages that cannot be processed normally |
| Transactional outbox | Atomically saves business data and the intent to publish |
BackgroundService | Hosts background processing, but isn't a durable broker |
| Order-processing architecture | Durable persistence, broker buffering, scalable consumers, recovery, and observability |
Recommended practice: Explain Q8 and Q10 aloud without looking at the notes. The transactional outbox and the complete order-processing design are particularly useful interview topics for a .NET/Azure backend role.
```
REST (Representational State Transfer) is an architectural style for designing APIs around resources and standard HTTP semantics.
GET /api/products/101
Authorization: Bearer <token>The API receives the product ID and authentication credentials in the request. It does not need to remember which server instance handled the user's previous request.
Suppose an application runs three API instances behind a load balancer:
Load Balancer
/ | \
API 1 API 2 API 3A user's first request might reach API 1 and the next request API 3. If requests are independently authenticated and the application doesn't rely on in-memory session state, any healthy instance can process them.
This improves horizontal scalability and simplifies failover.
Important distinction: Statelessness does not mean that the application has no state. Orders and user information can still be stored in SQL Server or another database. It means that each request does not depend on conversational state retained by a particular server instance.
Interview answer: “REST is a resource-oriented architectural style using standard HTTP semantics. Statelessness means every request contains the information required for processing, making it easier to scale APIs horizontally behind a load balancer.”
These are HTTP methods that express the intended operation on a resource.
These are HTTP methods that express the intended operation on a resource.
| Method | Purpose | Idempotent? |
|---|---|---|
| GET | Retrieve a resource | Yes |
| POST | Create a resource or submit an operation | Not guaranteed |
| PUT | Create or replace a resource at a known URI, depending on the API | Yes |
| PATCH | Partially modify a resource | Depends on the operation |
| DELETE | Delete a resource | Yes, by intended effect |
GET /api/products/101Returns a product without changing its business state.
POST /api/products
Content-Type: application/json
{
"name": "Keyboard",
"price": 1500
}Creates a product. Repeating the request may create duplicates unless the API implements deduplication or an idempotency mechanism.
PUT /api/products/101
Content-Type: application/json
{
"name": "Mechanical Keyboard",
"price": 2500
}Replaces the resource representation. Repeating the same request should leave the resource in the same state.
PATCH /api/products/101
Content-Type: application/merge-patch+json
{
"price": 2700
}Updates part of a resource. Setting a price to 2700 repeatedly is idempotent in its intended effect. An operation such as “increment price by 100” is not idempotent if repeated.
DELETE /api/products/101The intended effect is to delete the product. A subsequent request might return 404 Not Found rather than the original success status, but the resource remains deleted.
Network failures create uncertainty. A client might send a request, the server might complete it, and the response might get lost. The client then retries.
For order creation or payments, use an idempotency key or equivalent deduplication mechanism so retries do not create duplicate business effects.
Interview answer: “GET, PUT, and DELETE are defined as idempotent methods. POST is not guaranteed to be idempotent, and PATCH depends on the patch operation. Idempotency is especially important when clients retry requests after timeouts.”
Create(
| Status | Meaning | Example |
|---|---|---|
200 OK | Request succeeded with a response | Product retrieved |
201 Created | A resource was created | New product saved |
202 Accepted | Request accepted for processing, but processing is not necessarily complete | Report generation queued |
204 No Content | Request succeeded with no response body | Product deleted |
400 Bad Request | Request is invalid | Invalid query parameters |
401 Unauthorized | Authentication is missing or invalid | Invalid access token |
403 Forbidden | Caller is authenticated but lacks permission | User attempts an unauthorized administrative operation |
409 Conflict | Request conflicts with current resource state | Duplicate unique order reference |
429 Too Many Requests | Rate limit exceeded | Client exceeds its request quota |
[HttpPost]
public async Task<IActionResult> Create(
CreateProductDto request,
CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
return ValidationProblem(ModelState);
var product = await _productService.CreateAsync(
request, cancellationToken);
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product);
}This returns 201 Created and a Location header pointing to the newly created resource, assuming the GetById action is correctly configured.
For validation, ASP.NET Core controllers using [ApiController] can automatically return a 400 Bad Request when model validation fails.
401 means valid authentication has not been established; 403 means the authenticated caller is not allowed to perform the operation.201 means the resource has been created. 202 means the request has been accepted for processing, which may still be underway.For a queued order-processing API, returning 201 after persisting the order can be appropriate even if fulfillment is pending. Returning 202 can be appropriate when the API accepts a job for later execution and exposes a way to check its status.
Interview answer: “I choose status codes based on the API contract and the actual processing state. I use consistent validation and error responses, and I never expose internal exception details in production responses.”
p.Id)
Pagination prevents an API from returning an excessively large result set in one request.
Example:
GET /api/products?page=4&pageSize=20For one-based page numbers:
\\[ \text{offset}=(\text{page}-1)\times\text{pageSize} \\]
Page 4 with a page size of 20 means skipping 60 rows and returning the next 20.
ASP.NET Core with EF Core:
var page = Math.Max(1, requestedPage);
var pageSize = Math.Clamp(requestedPageSize, 1, 100);
var products = await db.Products
.AsNoTracking()
.OrderBy(p => p.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);Advantages:
Disadvantages:
Example:
GET /api/products?afterId=500&pageSize=20var pageSize = Math.Clamp(requestedPageSize, 1, 100);
var products = await db.Products
.AsNoTracking()
.Where(p => p.Id > afterId)
.OrderBy(p => p.Id)
.Take(pageSize)
.ToListAsync(cancellationToken);This example assumes IDs are unique and increasing. Production cursor pagination often returns an opaque cursor based on the last item's sort values.
Advantages:
Disadvantages:
| Factor | Offset | Cursor |
|---|---|---|
| Simplicity | Easier | More complex |
| Large datasets | Can be slow at large offsets | Often more efficient |
| Jump to page | Supported | Usually not |
| Changing data | More susceptible to shifts | More stable with a suitable cursor |
| Best use | Admin grids | Feeds, activity logs, large APIs |
Interview answer: “I use offset pagination when users need numbered pages and the dataset is manageable. For large datasets or feeds, I prefer cursor pagination using indexed, deterministic sort keys.”
query = db.Products.AsNoTracking();
I would design the endpoint so clients can retrieve only the records they need, while the database performs filtering, sorting, and pagination.
GET /api/v1/products?category=electronics&minPrice=500&maxPrice=5000&sortBy=price&sortDirection=asc&pageSize=20public sealed class ProductQuery
{
public string? Category { get; init; }
public decimal? MinPrice { get; init; }
public decimal? MaxPrice { get; init; }
public string SortBy { get; init; } = "id";
public string SortDirection { get; init; } = "asc";
public int PageSize { get; init; } = 20;
}Validate inputs, including price ranges and allowed sort fields. Clamp or reject excessive page sizes according to the API contract.
IQueryable<Product> query = db.Products.AsNoTracking();
if (!string.IsNullOrWhiteSpace(request.Category))
{
query = query.Where(p => p.Category == request.Category);
}
if (request.MinPrice.HasValue)
{
query = query.Where(
p => p.Price >= request.MinPrice.Value);
}
if (request.MaxPrice.HasValue)
{
query = query.Where(
p => p.Price <= request.MaxPrice.Value);
}
query = request.SortBy.ToLowerInvariant() switch
{
"price" => request.SortDirection == "desc"
? query.OrderByDescending(p => p.Price)
.ThenByDescending(p => p.Id)
: query.OrderBy(p => p.Price)
.ThenBy(p => p.Id),
_ => query.OrderBy(p => p.Id)
};
var pageSize = Math.Clamp(request.PageSize, 1, 100);
var products = await query
.Take(pageSize)
.Select(p => new ProductDto(
p.Id, p.Name, p.Price))
.ToListAsync(cancellationToken);This illustrates server-side filtering, sorting, projection, and a bounded result size. A real endpoint would also implement the chosen pagination contract—offset or cursor—and validate SortDirection explicitly rather than silently treating all unknown values as ascending.
For millions of rows:
AsNoTracking() for read-only EF Core queries.For offset pagination, a response might look like:
{
"items": [
{
"id": 101,
"name": "Keyboard",
"price": 1500
}
],
"page": 1,
"pageSize": 20,
"totalCount": 1250
}totalCount can be expensive for very large or complex filtered queries. Include it when the product requirement justifies the cost; cursor-based APIs often omit it.
Interview answer: “I would validate the query parameters, apply filters and ordering in SQL, use a bounded page size, project only required fields, and create indexes based on real query patterns. For deep pagination, I would consider cursor-based pagination and avoid expensive counts unless required.”
Rate limiting protects an API from excessive traffic and helps preserve capacity for legitimate requests.
Rate limiting protects an API from excessive traffic and helps preserve capacity for legitimate requests.
Allows a fixed number of requests during each time window.
Example: 100 requests per minute.
If a client sends 100 requests just before a minute ends and another 100 just after the next window starts, it can send 200 requests within a short period.
Best for: Simple quotas and straightforward policies.
Tracks requests across a rolling time interval or estimates usage using adjacent windows.
It reduces the fixed-window boundary problem.
Best for: More consistent request-rate limits.
Tokens replenish at a configured rate up to a maximum capacity. Each request consumes a token.
For example, a bucket may hold 20 tokens and replenish at 5 tokens per second. A client can make a burst of up to 20 requests when the bucket is full, while sustained traffic is constrained by the refill rate.
Best for: APIs that need to permit controlled bursts.
Limits the number of requests executing simultaneously, regardless of the number of requests received over a particular time window.
For example, an API might allow at most 50 concurrent report-generation requests.
Best for: Protecting expensive database queries, CPU-heavy operations, or constrained downstream services.
| Algorithm | Limits | Allows bursts? | Main benefit |
|---|---|---|---|
| Fixed window | Requests per fixed interval | Yes, near boundaries | Simplicity |
| Sliding window | Requests over a rolling interval | More controlled | Smoother rate enforcement |
| Token bucket | Average rate plus burst capacity | Yes, bounded | Flexible traffic shaping |
| Concurrency | Simultaneous operations | Depends on available slots | Protects processing capacity |
For a general public API, I might use a token bucket or sliding-window policy. For expensive operations, I would also consider a concurrency limiter. The right choice depends on whether the goal is to limit request frequency, bursts, or simultaneous work.
Interview answer: “A request-rate limiter controls how frequently a client calls an API, while a concurrency limiter controls how many requests execute simultaneously. I choose the algorithm based on the traffic pattern and the resource being protected.”
{
First, I would identify the caller reliably, then apply a rate-limit policy based on that identity.
For example:
The exact limits should come from capacity testing and business requirements.
ASP.NET Core supports partitioned rate limiting through AddPolicy and RateLimitPartition.
using System.Security.Claims;
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode =
StatusCodes.Status429TooManyRequests;
options.AddPolicy("PerUser", httpContext =>
{
var userId = httpContext.User
.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var partitionKey = userId ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0,
AutoReplenishment = true
});
});
});Register the middleware after routing and before mapping endpoints:
app.UseRouting();
app.UseAuthentication();
app.UseRateLimiter();
app.UseAuthorization();
app.MapControllers()
.RequireRateLimiting("PerUser");This is a simplified example. In a production application, anonymous users should not all share one partition if that would cause them to interfere with each other. Use a suitable fallback identity, and validate authentication and identity extraction.
Suppose there are three API instances:
Load Balancer
/ | \
API 1 API 2 API 3If each instance independently allows 100 requests per minute for a user, the user might effectively receive up to 300 requests per minute across all three instances.
The built-in in-process limiter does not automatically share counters between instances.
Also, do not trust a caller-supplied user ID or arbitrary X-Forwarded-For header as the identity. Authenticate users and configure trusted proxies correctly.
Interview answer: “I would partition requests by authenticated user or tenant and apply a suitable policy. For horizontally scaled deployments, I would enforce shared quotas at a gateway or use distributed coordination if a strict global limit is required.”
API versioning lets an API evolve while supporting existing clients that depend on older contracts.
API versioning lets an API evolve while supporting existing clients that depend on older contracts.
GET /api/v1/products
GET /api/v2/productsAdvantages: Explicit, easy to discover, and straightforward to route.
Trade-off: The version appears in the URL.
GET /api/products?api-version=1.0Advantages: The resource path stays unchanged.
Trade-off: The version is less visible in the route and must be handled consistently in routing and documentation.
GET /api/products
X-API-Version: 2Advantages: Keeps the URL clean.
Trade-off: The version is less visible when inspecting a URL alone. Caching and diagnostics must account for the header when responses differ by version.
A related approach is media-type versioning, using a versioned Accept header.
For a straightforward REST API, URL versioning is often easiest for clients and operations teams to understand. Query-string or header-based versioning can be suitable when it better fits the organization's API conventions.
One approach is explicit route templates:
[ApiController]
[Route("api/v1/products")]
public sealed class ProductsV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new[]
{
new { Id = 101, Name = "Keyboard" }
});
}
}A separate controller or endpoint can expose /api/v2/products with the newer contract.
For larger APIs, a versioning library can help with version selection, API Explorer integration, and OpenAPI documentation. The configuration depends on the library and versioning method.
Interview answer: “API versioning lets clients migrate without breaking existing integrations. I choose a clear versioning convention, keep each contract stable, document deprecation, and measure old-version usage before retirement.”
The key principle is backward compatibility: existing clients should continue to work when a new API release is deployed, unless a deliberate breaking change is being introduced through a migration plan.
The key principle is backward compatibility: existing clients should continue to work when a new API release is deployed, unless a deliberate breaking change is being introduced through a migration plan.
Version 1 response:
{
"id": 101,
"name": "Keyboard"
}A backward-compatible addition might be:
{
"id": 101,
"name": "Keyboard",
"category": "Electronics"
}Many clients will tolerate the additional field, but compatibility depends on the client and schema contract. Some strict deserializers or schema validators may reject unknown fields.
A breaking change would be renaming name to productName or changing the type of an existing field without maintaining compatibility.
An API can be backward-compatible at the HTTP layer but still fail if the database schema changes abruptly.
A common deployment strategy is expand and contract:
Interview answer: “I prefer additive API changes, maintain stable contracts, test compatibility, and use an expand-and-contract database migration strategy. For unavoidable breaking changes, I introduce a new version and deprecate the old one with clear timelines and usage monitoring.”
I would design a read-oriented API that supports efficient database queries, predictable response sizes, controlled traffic, and backward-compatible evolution.
I would design a read-oriented API that supports efficient database queries, predictable response sizes, controlled traffic, and backward-compatible evolution.
Functional requirements
Non-functional requirements
Client
Web app or mobile app
API Gateway
Authentication, quotas, request routing
ASP.NET Core API
Versioning, validation, filtering, pagination
Cache — optional
Redis for suitable repeated queries
SQL Server
Indexes, filtering, sorting, bounded queries
The cache is optional; I would introduce it when repeated reads justify the additional complexity and cache invalidation strategy.
Example request:
GET /api/v1/products?category=electronics&minPrice=500&maxPrice=5000&sortBy=price&sortDirection=asc&pageSize=20&afterId=500Example response:
{
"items": [
{
"id": 501,
"name": "Keyboard",
"price": 1500
},
{
"id": 502,
"name": "Mouse",
"price": 2000
}
],
"nextCursor": "opaque-cursor-value",
"hasMore": true
}The cursor is illustrative. In a real implementation, it would encode the last item's sort values, with validation and possibly signing. The example's afterId parameter is appropriate only for ID-based ordering; price ordering requires a cursor containing the relevant sort key and a unique tie-breaker.
I would:
AsNoTracking() for read-only queries.I would apply rate limits at the gateway or API layer, with separate policies for users or tenants and stricter limits for expensive queries.
When a client exceeds its quota, the API returns 429 Too Many Requests. For horizontally scaled deployments, shared limits may require gateway enforcement or distributed coordination.
I would use /api/v1/products for the current contract and introduce /api/v2/products when a breaking change is required.
I would document both versions, monitor old-version usage, communicate deprecation dates, and remove the old version only after the migration plan allows it.
Key metrics include:
“I would design a versioned ASP.NET Core product API behind an API gateway. The API would validate filter and sort parameters, enforce a maximum page size, and use indexed SQL queries with cursor pagination for large datasets. I would implement rate limiting to protect backend capacity, add Redis only where repeated reads justify it, and use metrics to identify bottlenecks. For breaking contract changes, I would introduce a new API version and manage the migration explicitly.”
Explain REST statelessness and horizontal scaling.
Know HTTP methods and idempotency.
Choose appropriate HTTP status codes.
Compare offset and cursor pagination.
Design indexed, bounded queries for large datasets.
Explain four rate-limiting algorithms.
Understand per-user limits across multiple API instances.
Compare URL, query-string, and header-based versioning.
Explain backward-compatible changes and expand-and-contract migrations.
Walk through the complete product API architecture.
0 of 10 revised
Reset
Next: Day 7 — Database Scaling, Replication, Sharding, and Consistency Patterns. This builds on the database fundamentals from Day 3 and applies them to larger system designs.
A systematic progression from engineering foundations to distributed systems and C# Low-Level Design.
Fundamental mental models, non-functional requirements, scaling dimensions, and system boundaries.
+ 5 more topics
The essential infrastructure components that power modern distributed web platforms.
+ 5 more topics
Tackling the hard realities of distributed coordination, network partitions, and data consistency.
+ 5 more topics
14 classic high-level system design interview scenarios with step-by-step architectural blueprints.
+ 9 more topics
Object-oriented principles, design patterns, and clean architecture implementations in modern C#.
+ 3 more topics
Never memorize diagrams. Approach any unfamiliar system design question using this structured sequence.
Separate functional requirements (what it does) from non-functional requirements (scale, latency, availability, durability).
Estimate Daily Active Users (DAU), Read vs Write ratio, Requests Per Second (RPS), throughput, storage requirements over 5 years.
Define clean REST or gRPC endpoint contracts with request payloads, query parameters, idempotency headers, and HTTP status codes.
Design schemas, choose relational vs document vs key-value vs time-series, identify primary keys and indexing strategies.
Draw end-to-end components: Clients → DNS/CDN → Load Balancer → API Gateway → Application Services → Cache → Database → Message Queue.
Zoom into the highest-risk modules: unique ID generation, consistent hashing ring, caching policy, or queue consumer processing.
Identify database write limits, memory bounds, hot partitions, network saturation, and SPOF components.
Explain CAP choices (AP vs CP), why Cache-Aside over Write-Through, SQL vs NoSQL, and latency vs consistency trade-offs.
Plan for network partitions, node crashes, circuit breakers, dead-letter queues, telemetry, and health check probes.