No API-to-API dependency
Ingestion and consolidation are independent boundaries. Reads must not depend on write API availability.
Architectural case study · public laboratory
dotnet-observability-lab is an educational reference architecture I use to make a complete reasoning chain inspectable: problem → constraints → quality attributes → decisions → implementation → failures → observability → evidence.
This page is the portfolio reading. The canonical technical documentation remains in the lab repository.
Problem
An API receives a value and must accept it durably and idempotently. Another service boundary must consolidate that value asynchronously, without API-to-API calls and without turning broker redelivery into a duplicated business effect.
The real difficulty is not the decimal value. It is the interval between persisting, publishing, consuming, acknowledging, and recovering — including cases where the previous step has an uncertain outcome.
Constraints and quality attributes
Ingestion and consolidation are independent boundaries. Reads must not depend on write API availability.
RabbitMQ may redeliver. Duplicates are part of the contract and are treated as normal behavior.
PostgreSQL protects idempotency, Outbox, Inbox, and aggregate state. Redis is only a best-effort shortcut.
OpenTelemetry defines traces, metrics, and logs. Aspire composes and displays the local environment but does not route business traffic.
Decisions and trade-offs
RabbitMQ decouples availability and latency between ingestion and consolidation.
Trade-off: eventual consistency and higher operational cost.
ADR 0004 ↗The value and pending event commit in one transaction; a worker publishes later with broker confirmation.
Trade-off: polling, retries, quarantine, and possible repeated publication.
Implementation ↗Message identity and aggregate update are committed together in PostgreSQL.
Trade-off: every unique message requires durable work and a future retention policy.
Implementation ↗The design does not claim exactly-once transport. It makes repetition safe at the business-effect level.
Trade-off: every consumer must model identity and duplication explicitly.
Event contract ↗Cache reduces duplicate HTTP work, but eviction, restart, or outage cannot alter correctness.
Trade-off: cache misses and failures may still hit the database.
ADR 0003 ↗W3C context crosses the asynchronous boundary; Aspire is one local inspection surface.
Trade-off: propagation and instrumentation must be maintained explicitly.
ADR 0005 ↗Nominal flow
Relevant failures
Same key + same value returns the original receipt; conflicting reuse returns 409. PostgreSQL uniqueness settles concurrent races.
The Inbox prevents a second business effect. A message may be consumed again without incrementing the aggregate again.
The API can still accept and persist the write. Outbox work remains pending for later publication.
The fast path disappears, but correctness does not. Idempotency falls back to PostgreSQL.
The event may be published again with the same identity; the consumer safely handles the duplicate.
Inbox and aggregate roll back together. No false processed marker remains after a failed transaction.
Observability
The W3C context started by the request is persisted with the Outbox, resumed by the publisher, and propagated through AMQP headers to the consumer. Low-cardinality metrics distinguish acceptance, publication, processing, and duplication; structured logs capture relevant operational outcomes.
The goal is not to “have dashboards.” It is to explain why an accepted write has not reached the read model and identify the boundary where the flow is blocked or repeating.
See trace and metric tests ↗Public evidence
Deliberate limits
The lab keeps one local PostgreSQL server with separate logical databases, does not prescribe a production deployment platform, and does not implement a complete bounded consumer retry/DLQ policy in v1. It also makes no event-ordering or exactly-once claim.
Those limits are part of the evidence: they show what was decided, what was simplified for educational use, and which choices would need to be reopened in a real system.
Explore the full repository ↗