Framework for .NET System Design
Key Points
- A repeatable framework: clarify requirements → estimate capacity → design API → design data → design distribution → resilience → observability → trade-offs.
- For interviews and real systems alike. Each step has .NET-specific defaults.
- Don't jump to the design — clarify, estimate, narrate. Senior calibration is in the questions.
- Time-box each step. 45-min interview: 5 clarify, 5 estimate, 10 API, 10 data, 5 distribution, 5 trade-offs, 5 resilience+ops.
The framework (deep dive)
1. Clarify requirements (5 min)
Functional: - What does the system do? Use cases? - Who are the users? Internal / external? - What's success?
Non-functional: - Reads/writes per second? Day/peak? - Latency budget (p50, p99)? - Availability SLO (99.9% / 99.99%)? - Consistency: strong / eventual? - Geography: single region / multi-region / global? - Compliance: PII, HIPAA, PCI?
Constraints: - Existing systems (DB? IdP? cloud)? - Budget? - Team's stack (definitely .NET? language constraints)?
Senior tip: explicitly say "Let me clarify a few things first" and ask 4-6 sharp questions. Drives the entire design.
2. Capacity estimation (5 min)
Users: 10M total, 1M DAU, 100K concurrent peak
Reads / write ratio: 100:1
RPS peak: 100K * 5 actions/min / 60 = ~8K read RPS, ~80 write RPS
Storage:
per-record: 1 KB
10M records * 1 KB = 10 GB
growth: 1M new/month → 1 GB/month → 12 GB/year
Bandwidth:
8K req/s * 5 KB response = 40 MB/s = 320 Mbps
Round numbers; show working. Frames the rest.
3. API design (10 min)
GET /v1/users/{id} → returns User
POST /v1/users → idempotency key required
PUT /v1/users/{id} → If-Match for ETag
GET /v1/users?cursor=&limit=50 → cursor pagination
DELETE /v1/users/{id} → soft delete
- Versioning (URL path).
- Auth (JWT bearer + scopes).
- Pagination (cursor for live data).
- Errors (ProblemDetails RFC 9457).
- Idempotency (header for non-idempotent).
- ETags (concurrency).
For real-time: SignalR hubs. For sync RPC: gRPC. For events: Service Bus / Kafka.
4. Data design (10 min)
Schema:
User
├── Id (PK)
├── Email (unique index)
├── CreatedAt (index for time-range queries)
└── ...
Order
├── Id (PK)
├── UserId (FK + index)
├── Status (index for queue queries)
└── ...
Database choice:
| Need | Pick |
|---|---|
| Transactional, relational | Postgres / SQL Server |
| Global distribution, NoSQL | Cosmos DB |
| High-write append | Kafka / Event Hubs |
| Cache | Redis |
| Search | Elasticsearch / Azure AI Search |
| Vector | Cosmos / Postgres pgvector / Qdrant |
Indexes: by query pattern. Don't over-index — write cost.
Partitioning (Cosmos): high-cardinality key. Avoid hot partition.
5. Distribution (5 min)
Components:
Patterns: - Read replicas for read scaling. - Cache in front (HybridCache). - Outbox for events. - CQRS if read/write asymmetric. - Saga for multi-service workflows.
6. Resilience (5 min)
- Polly v8 for retries / circuit breaker / timeouts.
- Idempotency for safe retries.
- DLQ for poison messages.
- Health checks + autoscaling.
- Backpressure with bounded buffers.
- Multi-region for high SLO.
7. Observability (5 min)
- Logs: structured, correlation IDs.
- Metrics: RPS, latency p50/p99, errors %.
- Traces: distributed via OTel.
- Alerts: budget-burn-rate alerts on SLO.
8. Trade-offs (5 min)
Always explicit:
Cost vs latency: cache more = fast but stale.
Strong consistency: lower availability under partition.
Single region: cheaper, lower SLO.
Cosmos DB: globally distributed, but RUs can be expensive.
Senior signal = naming the alternatives you considered + why they're worse.
Monolith vs Modular Monolith vs Microservices
The architectural axis under almost every system-design discussion. Most teams should default to Modular Monolith.
ops overhead →
low high
│
│ Monolith ──→ Modular Monolith ──→ Distributed Modular ──→ Microservices
│ (single (single deploy, (a few services, (many services,
│ deploy, bounded modules) per-team boundaries) per-feature deploys)
│ single
│ DB)
▼
deployment coupling ↓
| Aspect | Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deploy unit | 1 | 1 | N (independent) |
| DB | 1 shared | 1 (sometimes per-module) | DB-per-service |
| Inter-module/service | In-process calls | In-process via interfaces | Network (REST/gRPC/messaging) |
| Team scaling | Hard past ~20 devs | OK to ~50-100 with module owners | Designed for team-of-teams |
| Ops overhead | Low | Low | High (distributed tracing, contract tests, deploy pipelines per service) |
| Refactor cost | Low (compiler helps) | Low (compiler helps across modules) | High (network + versioning + data migrations) |
| Best for | Early-stage, small team, simple domain | Most non-megascale .NET shops in 2026 | Independent release cadence, separate scaling targets, true team autonomy |
- Monolith: single deployable, single codebase, single DB. Fastest to ship; lowest ops cost; refactor-friendly. Pain shows up at team-scale, traffic-on-hot-paths, deployment coupling, shared-DB contention.
- Modular Monolith: single deployable, but with bounded internal modules with explicit interfaces (.NET assemblies, Vertical Slice or Clean per module, sometimes per-module databases). The senior recommendation for most non-megascale shops. See Vertical Slice Architecture and Clean / Onion / Hexagonal.
- Microservices: independent deployables, separate DBs (database-per-service), inter-service via messaging or REST/gRPC. High operational overhead — observability, deployment pipelines, contract testing, distributed tracing, retry/idempotency, service discovery. Win when team-of-teams, independent release cadences, true scale demands force it.
💡 Conway's Law: org structure tends to follow architecture (and vice versa). If you have one team, microservices fight your org chart. If you have ten autonomous teams, a monolith fights theirs.
Decision rule: start with Modular Monolith. Extract a service only when there's a concrete reason — independent scaling target, regulatory isolation, polyglot stack, separate team ownership, deploy-cadence mismatch. Don't microservices-by-default. The cost of premature decomposition is paid in tracing dashboards forever.
Horizontal vs Vertical scaling
Two axes of "make it handle more load." Senior insight: scale vertically until there's a clear reason to scale horizontally.
▲
│ horizontal: more boxes
│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
│ │ │ │ │ │ │ │ │ │ │ ← stateless tier
│ └──┘ └──┘ └──┘ └──┘ └──┘
│
vertical: │ ┌──────┐
bigger box │ │ DB │ ← stateful tier (usually scales vertical first)
│ │ │
│ └──────┘
└─────────────────────────────►
- Vertical (scale up): bigger box. CPU, RAM, IOPS. Wins single-machine perf (no network overhead between threads, shared L3 cache, tight latency). Limit: hardware ceiling and price-per-unit climbs steeply at the top end (top-tier SKUs cost more than 2× the next-tier-down for ~30% more capacity).
- Horizontal (scale out): more boxes. Requires statelessness (or partitioned state); load-balancer in front; data layer becomes the bottleneck. Cheap incremental capacity, but you import distributed-systems pain — eventual consistency, sticky sessions, distributed locks, deployment ordering.
The senior insight: scale vertically until there's a clear reason to scale horizontally. Going horizontal early imports distributed-systems complexity you don't need. A 96-vCPU VM with NVMe will out-throughput your half-baked sharded cluster every time.
Stateful tier (DB): vertical first → read replicas → caching → partitioning/sharding only when forced. Cross-link: Multi-Region Replication.
Compute tier (API/workers): vertical until ~$1k/mo VM tier, then horizontal once stateless. Cross-links: Kubernetes for .NET, Azure App Service & Functions.
| Workload | Preferred scaling |
|---|---|
| Stateless API / web tier | Horizontal (after 1 box is right-sized) |
| Background workers / queue consumers | Horizontal (partition by queue / partition key) |
| RDBMS primary | Vertical + read replicas; shard only if forced |
| Cache (Redis) | Vertical first; cluster mode when single node saturates |
| Search (Elasticsearch / Azure AI Search) | Horizontal (sharded by design) |
| Event broker (Kafka, Event Hubs) | Horizontal (partitions are the scaling unit) |
| GPU / ML inference | Vertical per box (bigger GPU); horizontal for throughput |
⚠️ Don't conflate "cloud-native" with "must be horizontal." A right-sized vertical compute tier with autoscale 1→3 horizontal at peak is the answer for most .NET workloads.
Common .NET-specific defaults
Web framework: ASP.NET Core (Minimal APIs)
Auth: JwtBearer + AAD or Auth0
DB: Postgres + EF Core, or Cosmos
Cache: HybridCache (memory + Redis)
Queue: Service Bus or Kafka
Real-time: SignalR + Redis backplane (or Azure SignalR)
Resilience: Microsoft.Extensions.Resilience (Polly v8)
Observability: OpenTelemetry → Azure Monitor / Datadog
Hosting: Container Apps or AKS
IaC: Bicep
CI/CD: GitHub Actions + OIDC
State these as defaults; deviate if the case demands.
Questions to ask
1. What's the read:write ratio?
2. What's the latency budget at p99?
3. Is the data sensitive (PII / regulated)?
4. Single region or global users?
5. Existing infrastructure / cloud / IdP?
6. Team's familiar stack?
7. SLO for availability?
8. Anything that can fail (downstream services, third parties)?
Diagrams in interviews
ASCII or whiteboard. Layer diagram + component diagram + sequence for tricky flows.
[Client]
│ HTTPS
▼
[CDN / Front Door]
│
▼
[YARP / APIM]
│
▼
[ASP.NET Core API (3 replicas)]
│
├──→ [Postgres primary + 2 replicas]
├──→ [Redis cache]
├──→ [Service Bus] → [Worker]
└──→ [App Insights / OTel collector]
Time management
- Don't dive into code. Pseudocode only when illustrative.
- Whiteboard a tree of options, not a decision.
- Narrate trade-offs as you draw.
Common interview pitfalls
- Skipping clarification.
- Over-engineering for "scale" without justification.
- Ignoring failure modes.
- Not stating SLAs / metrics.
- Hand-wave on consistency.
Senior signals
- Asking the right clarifying questions.
- Picking right-sized tech (not always Kafka).
- Naming trade-offs explicitly.
- Considering ops (deploy, observe, debug).
- Mentioning cost.
- Considering team / stack constraints.
Code: correct vs wrong
Not applicable for framework — but for interview narration:
❌ Wrong
"I'd use Kafka for everything because it's distributed."
✅ Correct
"RPS is 100. Service Bus is plenty; simpler ops. Kafka is overkill unless we need replay."
Design patterns for this topic
Pattern 1 — "Clarify-Estimate-Design"
- Intent: structured approach.
Pattern 2 — ".NET-default stack"
- Intent: consistent baseline.
Pattern 3 — "Trade-off narration"
- Intent: demonstrate senior thinking.
Pattern 4 — "Right-sizing tech"
- Intent: avoid over-engineering.
Pattern 5 — "Capacity math"
- Intent: ground design in numbers.
Pros & cons / trade-offs
For the framework itself:
| Aspect | Pros | Cons |
|---|---|---|
| Structured | Demonstrates rigor | Time pressure |
| Trade-offs explicit | Senior signal | Slower if rushed |
| .NET defaults | Consistent baseline | May not fit every case |
When to use / when to avoid
- Always for system-design questions.
- Adapt to actual scale of problem.
Interview Q&A
Q1. First step? Clarify requirements. Don't design before knowing what.
Q2. Why capacity estimate? Drives DB choice, replica count, cache strategy. Round numbers OK.
Q3. Default DB? Postgres. Cosmos for global, schema-flexible.
Q4. Default queue? Service Bus for business; Event Hubs for streaming.
Q5. Real-time? SignalR + backplane. Or raw WebSockets if specific.
Q6. Auth default? JWT Bearer + AAD / Auth0. BFF for SPA.
Q7. Resilience defaults? Polly standard handler + idempotency + DLQ + circuit breaker.
Q8. Observability defaults? OpenTelemetry → Azure Monitor / Datadog. Logs + metrics + traces.
Q9. Multi-region — when? SLO > 99.9%; latency-sensitive global users; regulatory.
Q10. Cost trade-offs? Caching reduces DB load (cheaper) but stale risk. Premium plans cost; consumption tier cheap idle.
Q11. SaaS multi-tenancy? Row-per-tenant + EF query filters; or schema-per-tenant; or DB-per-tenant. Trade-offs in case file.
Q12. RAG? ingest → chunk → embed → store → retrieve → rerank → prompt → LLM. Microsoft.Extensions.AI.
Gotchas / common mistakes
- ⚠️ Diving in without clarification.
- ⚠️ Over-engineering for unbounded scale.
- ⚠️ No capacity math.
- ⚠️ Hand-wave on consistency.
- ⚠️ No trade-off discussion.