Skip to content

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:

[Client] → [LB] → [API instances 3+] → [DB primary + replicas]
                                       → [Cache]
                                       → [Queue]
                                       → [Workers]

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.

Further reading