Skip to content

Untitled

Starting Point

Fresh session. No prior context on API design. Goal: structured interview preparation across all bands of API design knowledge. User is a senior .NET/C# engineer who has built real APIs but never had a framework for scoping or structuring API design questions in interviews.


What We Did

Band 1 — Fundamentals

HTTP Methods

Established correct method mapping for resources. Key correction: user defaulted to PUT for order status update — corrected to PATCH. Derived the precise distinction:

  • PUT = client owns the entire resource, sends full replacement
  • PATCH = server owns some fields, client sends only what changes
  • PUT is natural for file upload and config documents. Rarely correct for domain entities because the server always owns some fields (createdAt, internal state, etc.)

Worked through sub-resource vs field distinction using delivery address as the example:

  • Order's delivery address — value object, no identity, PATCH on the order
  • Customer's address list — child entity, has identity, own sub-resource, PUT to replace one address

Connected REST resource design back to DDD: aggregate roots map to top-level resources, value objects are fields in the body, child entities are sub-resources.

Status Codes

Covered all groups. Key points landed:

  • 401 = unauthenticated (named "Unauthorized" but means "I don't know who you are")
  • 403 = authenticated but forbidden
  • 404 vs empty collection — GET /users/100 not found = 404, GET /users?name=x no results = 200 with empty array
  • 202 = accepted for async processing — the HTTP equivalent of putting something on RabbitMQ and returning immediately
  • 410 Gone vs 404 — resource existed and is permanently gone vs never heard of it
  • 422 = valid request, business rule says no
  • 429 = rate limiting
  • 5xx boundary — 500 is yours, 502/503/504 are Nginx's

Soft deleted resources: 404 from the consumer API perspective. Storage detail is irrelevant.

URL Design

Covered all conventions:

  • Nouns not verbs — method carries the action
  • Plural nouns always
  • Hierarchy expresses ownership — two levels max
  • Lowercase, hyphens
  • Query strings for filtering/sorting/pagination
  • Sub-resource POSTs for domain actions that don't fit CRUD (/orders/:id/cancel)

Hierarchy depth rule: once a resource has its own identity it can be a top-level resource. The test — would you ever fetch this without knowing its parent?


Band 2 — Design Decisions

Versioning

URL path versioning — /v1/, /v2/. Shared service layer, versioned at controller level. Separate logical clustering per version, not duplicated business logic.

Breaking vs non-breaking:

  • Non-breaking: optional fields, new endpoints, additive response fields
  • Breaking: removing/renaming fields, changing types, making optional fields required, changing semantic meaning

Real ERP example: totalAmountamount + currencyCode, customerNamecustomerId. Breaking on both — removal and type change. Requires v2.

EOL strategy: monitor v1 traffic, set sunset date when substantially low. Deprecation: true and Sunset headers signal consumers ahead of time. Return 410 Gone after sunset, not 404.

Pagination

Offset pagination: page, pageSize, totalCount. Problem: inserts between pages cause duplicates/skips.

Cursor pagination: opaque base64 encoded pointer to last record seen. Stable under inserts. Loses totalCount and random page access.

Cursor encodes: sort field value + ID as tiebreaker for non-unique sort values. ID-only cursor works when sorting by ID. Multi-sort cursors encode the sort field and value inside the cursor itself.

Filters go as separate query params alongside cursor. Changing filters mid-pagination invalidates the cursor — treat as fresh query or encode filters inside cursor and validate.

When to use each:

  • Offset: grids, reports, anything with explicit sorting and page jumping
  • Cursor: audit trails, feeds, webhook logs, anything requiring stability under inserts

Error Response Format

RFC 7807 — Problem Details for HTTP APIs.

{
  "type": "https://api.yourapp.com/errors/order-already-shipped",
  "title": "Order Cannot Be Modified",
  "status": 422,
  "detail": "Order 123 has already been shipped and cannot be updated.",
  "instance": "/orders/123",
  "traceId": "00-7b3f2a1c4d5e6f7a-8b9c0d1e2f3a4b5c-00"
}

Validation errors extend with errors field — map of field name to array of messages. ASP.NET Core generates RFC 7807 compliant responses with AddProblemDetails().

Idempotency

Client generates key from business identity — not from the action. UUID v5 (deterministic, SHA-1 hash of namespace + name) for derived keys. UUID v4 (random) is wrong for idempotency keys because double-click generates two different keys.

UUID versions worth knowing:

  • v4: random, general purpose
  • v5: deterministic from namespace + name
  • v7: timestamp + random, sortable, useful for cursor pagination

Server stores key + status + response. States: processing → complete / failed.

Race condition fix: atomic insert with unique constraint. PostgreSQL INSERT ... ON CONFLICT DO NOTHING. Redis SET NX. First write wins, second gets 409.

Failure handling: status column on the key. Failed = reprocess on retry. Wrap operation + key update in a transaction so partial failures leave no state.

Retention: 24 hours for retry deduplication. Forever for compliance (PEPPOL).

Important distinction: PEPPOL implementation is not a pure idempotency key — it's a domain state check. Unique constraint on invoiceNumber + tenantId + channelId. Protects against duplicates regardless of how the request arrived, not just HTTP retries. More robust because it's domain-level not infrastructure-level.


Band 3 — Contracts and Consistency

API Contract

A contract is the behaviour consumers depend on, not the code. Breaking the contract means changing behaviour a consumer relies on regardless of how small the change seems.

Deprecation Strategy

  1. Signal with Deprecation: true and Sunset headers
  2. Document in changelog and release notes
  3. Monitor traffic — set EOL when substantially low
  4. Return 410 Gone after sunset

API-First Design

Design the OpenAPI spec before writing code. Frontend and backend develop in parallel against mocks. Code implements the spec, not the other way around. Prevents API shape from being an accident of implementation details.

In .NET: code-first with Swashbuckle (common), API-first with NSwag or Kiota (spec is source of truth).

Stable API Rules

Additive changes always safe. Anything removed, renamed, or type-changed requires a version. Postel's Law: liberal in what you accept, conservative in what you send.

Consumer-Driven Contracts

Pact framework. Consumers define what they need, producer must satisfy all consumer contracts. Build fails if producer breaks any consumer's contract. Relevant when multiple teams consume your API — automates the guarantee instead of relying on coordination.


Band 4 — Hard Tradeoffs

REST vs gRPC vs GraphQL

REST: public APIs, CRUD-heavy domains, universal tooling. Struggles with over/under-fetching.

gRPC: internal service-to-service, binary Protocol Buffers over HTTP/2, strongly typed contracts from proto files, supports streaming. Doesn't work natively in browsers. RabbitMQ messages are conceptually RPC over a message bus — gRPC is the synchronous equivalent.

GraphQL: client-driven queries, one endpoint, client defines exact shape needed. Solves over/under-fetching. Complex caching, N+1 problem, query depth attacks. Overkill for simple CRUD.

Decision: REST for public/browser APIs, gRPC for internal service-to-service, GraphQL for complex frontends with varied data needs per screen.

HATEOAS and Richardson Maturity Model

Level 0: one endpoint, everything POST (SOAP) Level 1: separate URLs per resource Level 2: HTTP verbs — what most production APIs are Level 3: HATEOAS — responses include next valid actions

HATEOAS: theoretically decouples clients from URL structure. In practice: clients hardcode URLs anyway, response bloat, complex to maintain. Almost nobody implements it.

Rate Limiting

Algorithms: fixed window (simple, burst problem at boundaries), sliding window (smoother), token bucket (allows controlled bursting, most common in practice), leaky bucket (fixed processing rate, smooths traffic).

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After. Status code: 429.

Webhooks vs Polling vs SSE

Polling: client asks repeatedly. Simple, wasteful, latency equals poll interval.

Webhooks: server POSTs to client-registered URL on event. Efficient, real-time. Client must be publicly accessible. PEPPOL relay is a webhook receiver.

SSE: HTTP connection stays open, server pushes events. One-directional. Works in browsers. Natural fit for ops dashboard real-time updates.

WebSockets: bidirectional. Overkill unless client also pushes.


End-to-End Design Questions

Framework derived across three questions:

  1. Domain first — entities, states, actors
  2. Relationships — what owns what, aggregate boundaries
  3. Endpoints — resources fall out naturally from domain
  4. Hard problems — surface them yourself before being asked
  5. Deep dive — wherever interviewer steers

Doctor Appointment System

Key decisions: slot as separate entity because doctor pre-defines discrete availability. Appointment references slot. Concurrency on booking: unique constraint on slotId, first insert wins, second gets 409. Access control scopes queries server-side — same endpoint for all actors, server forces filter based on caller identity.

Food Delivery System

State machine: created → accepted → prepared → driver_assigned → picked_up → delivered. Driver self-assigns from pool of prepared orders in vicinity. Concurrency on driver assignment: conditional UPDATE WHERE status = 'prepared', zero rows updated = 422. Location on restaurant entity for proximity queries. Push over poll for driver notification — SSE or mobile push to drivers in radius.

Hotel Booking System

Slot not needed — availability is continuous and derived from absence of overlapping bookings. SQL: NOT IN subquery on bookings for date range overlap. Availability as computed field on room response vs explicit /rooms/:roomId/availability endpoint — explicit is cleaner, computed field is implicit and surprising. Flattened URL for single resource access, nested for collection browsing.

DDD discussion: Room can be child entity inside Hotel aggregate (hotel management context) or own aggregate root (room operations context). Same entity, different boundary depending on which invariants need enforcing. Booking aggregate owns only its own state machine — references customer, room, hotel by ID only. BookingStatusHistory is a legitimate child entity if audit trail is needed.


Concepts Covered

Value object vs entity vs aggregate root — REST mapping Value object = field in body. Child entity = sub-resource. Aggregate root = top-level resource. The domain model is the source of truth for API resource design, not REST conventions.

PUT vs PATCH — the precise rule Not "full vs partial." Client owns entire resource = PUT. Server owns any field = PATCH. In practice PATCH is almost always correct for domain entities.

404 vs 422 vs 409 404: resource doesn't exist. 422: resource exists, business rule says no. 409: conflict with current state (concurrent operation in progress).

Idempotency key vs domain state check Key is infrastructure-level retry deduplication. Domain state check is business-level deduplication. Domain state check is more robust — protects against all duplicate sources not just HTTP retries.

Slot entity vs derived availability Discrete pre-defined availability = slot entity needed. Continuous date-range availability = derived from absence of bookings, no slot needed.

Aggregate boundaries are context-dependent Same concept can be aggregate root in one bounded context, child entity in another. Pick the boundary that matches the invariants being enforced, not a single model for the whole system.


What We Messed Up

PUT for order status update Reached for PUT instinctively. Wrong — status is one field on the order, PATCH is correct. Fix: method follows what the body represents, not how important the field is.

Idempotency key as random GUID A GUID generated on click produces a new key every click — useless for deduplication. Fix: key must be derived from business identity using UUID v5. Same inputs always produce same key.

Availability as separate Boolean endpoint Reached for a separate check endpoint. Unnecessary — either filter the rooms collection or add available as a computed field on the room response. Better still — explicit /rooms/:roomId/availability endpoint if clarity matters. The operation itself (POST /bookings) is the ultimate availability check.

410 for already-assigned order Reached for 410 Gone. Wrong — the resource still exists, it's a business rule rejection. Fix: 422 for any state transition that violates the state machine.

Nested URL depth Instinct to nest everything deeply. Fix: nest one level for collections to express ownership, flatten for individual resource access when the ID is sufficient.


Key Values and Config to Remember

Concept Value
RFC for error format RFC 7807 — Problem Details
RFC for PATCH merge semantics RFC 7396 — JSON Merge Patch
RFC for PATCH operations RFC 6902 — JSON Patch
Idempotency key retention 24 hours standard, forever for compliance
Deprecation headers Deprecation: true, Sunset: <date>, Link: <successor>
Post-sunset status code 410 Gone
Rate limit exceeded 429 Too Many Requests
Richardson Level 2 Resources + HTTP verbs — what most APIs are
Richardson Level 3 HATEOAS — almost nobody implements
UUID v5 Deterministic, SHA-1, namespace + name
UUID v7 Sortable, timestamp + random, .NET 9 Guid.CreateVersion7()

Unanswered Questions / Things to Investigate

  • Band 3 questions not drilled — user acknowledged understanding but no interview-style questions were asked. Worth revisiting with mock questions.
  • Consumer-driven contracts with Pact — covered conceptually, never applied. Worth knowing what a pact file looks like.
  • GraphQL N+1 problem — mentioned but not explained. DataLoader pattern solves it.
  • gRPC in .NET — proto file structure, code generation, how it compares to REST controllers in practice.
  • OpenAPI spec first workflow in .NET — NSwag or Kiota, what the actual workflow looks like.
  • Hotel availability SQL — date range overlap query worth writing out and understanding properly.
  • SSE implementation in ASP.NET Core — natural fit for ops dashboard, worth a practical look.

What's Next

  1. Band 3 mock questions — do it interview style, no study mode. Versioning, deprecation, API-first, stability.
  2. One more end-to-end design question — under timed conditions, no prompting. Test whether the framework holds without guidance.
  3. gRPC basics — proto file, service definition, code generation in .NET. One practical example sufficient.
  4. GraphQL N+1 — understand the problem and DataLoader solution. Enough to discuss tradeoffs confidently.
  5. SSE in ASP.NET Core — implement a basic event stream. Connects directly to your PEPPOL ops dashboard.