Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Read this first when picking up work.** It's the cross-session entry point: where the project is right now, what to do next, and where the deeper docs live. Keep it short (~100 lines). Update it at the start or end of each working session.

**Last updated:** 2026-05-25
**Last updated:** 2026-05-25 (per-service code-flow walkthroughs + cache-backplane reframing)

---

Expand Down Expand Up @@ -138,11 +138,12 @@ Conditional follow-up — only matters once we deploy more than one replica of a

**The problem.** `Microsoft.Extensions.Caching.Hybrid` 10.x has no backplane. When replica A invalidates a `ProductDto`, replicas B/C continue serving the stale value from their own in-process L1 for up to `LocalCacheExpiration` (currently 5 min). The API proposal for a pluggable backplane ([dotnet/extensions#5517](https://github.com/dotnet/extensions/issues/5517)) was closed as "NOT ready for implementation" — not coming soon.

**Mitigation, cheapest first:**
1. **Drop `LocalCacheExpiration` to 60s** in [HybridProductCache.cs](../CatalogService/CatalogService.Infrastructure/Caching/HybridProductCache.cs). One-line change. Bounds cross-replica staleness at 60s. We lose part of the L1 win for the warm-but-aging tail of entries but keep the hot-entry win and the L2 win. **This is the right move for "ship multi-replica with reasonable consistency."**
2. **Migrate to [FusionCache](https://github.com/ZiggyCreatures/FusionCache)** if 60s isn't tight enough. FusionCache ships a Redis pub/sub backplane that publishes invalidations to all replicas — drop-in functional replacement with the consistency story we originally wanted from HybridCache. Wiring change is moderate: swap package, retarget the `IProductCache` adapter, verify metrics still flow through OTel. Estimate ~half day plus a chaos test. The cache *seam* (`IProductCache`) stays the same; handlers don't change.
**Mitigations, cheapest band-aid first → proper fix last:**
1. **(Band-aid) Drop `LocalCacheExpiration` to 60s** in [HybridProductCache.cs](../CatalogService/CatalogService.Infrastructure/Caching/HybridProductCache.cs). One-line change, bounds cross-replica staleness at 60s. Trade: lose part of the L1 win for the warm-but-aging tail of entries, keep the hot-entry win and the L2 win. **Acceptable for "ship multi-replica with reasonable consistency this sprint," but explicitly a band-aid** — per [Milan Jovanović: *Solving the distributed cache invalidation problem with Redis and HybridCache*](https://www.milanjovanovic.tech/blog/solving-the-distributed-cache-invalidation-problem-with-redis-and-hybridcache), shorter TTL doesn't fix the problem, it just shrinks the inconsistency window. For permissions, pricing, or feature flags the residual staleness is not OK. For product catalog reads it's tolerable.
2. **(Proper fix, custom backplane) Hand-roll Redis Pub/Sub backplane**, ~50-100 lines. Add an `ICacheInvalidator` registered alongside `IProductCache`; its impl publishes the cleared cache key to a `cache-invalidation` Redis channel via the existing `IConnectionMultiplexer`. An `IHostedService` subscribes, calls `HybridCache.RemoveAsync(key)` locally on every replica when a message arrives, ignores its own publishes. Reuses our existing Redis dependency — no new infrastructure. Self-publishes are redundant but harmless. Walks the same shape as the article's panel 3-4.
3. **(Proper fix, migrate) Swap to [FusionCache](https://github.com/ZiggyCreatures/FusionCache)** if we want the backplane out-of-the-box. FusionCache ships a built-in Redis Pub/Sub backplane and `.AsHybridCache()` shim so call sites that depend on `Microsoft.Extensions.Caching.Hybrid.HybridCache` keep working. Cleaner long-term; heavier short-term (new package, OTel re-verify, chaos test). Estimate ~half day.

**Filed here, not in "After the smoke run,"** because this only matters once there's an actual multi-replica deployment. Don't pre-optimize for cross-replica before there's a real cross-replica. Background reading: [Tim Deschryver: FusionCache backplane synchronizing HybridCache](https://timdeschryver.dev/blog/hybridcache-sync-with-fusioncache-backplane).
**Filed here, not in "After the smoke run,"** because this only matters once there's an actual multi-replica deployment. Don't pre-optimize for cross-replica before there's a real cross-replica. Background reading: [Milan Jovanović: *Distributed cache invalidation with Redis + HybridCache*](https://www.milanjovanovic.tech/blog/solving-the-distributed-cache-invalidation-problem-with-redis-and-hybridcache) (the rationale for #2 and against #1 as a permanent solution); [Tim Deschryver: FusionCache backplane synchronizing HybridCache](https://timdeschryver.dev/blog/hybridcache-sync-with-fusioncache-backplane) (the rationale for #3).

---

Expand Down
103 changes: 103 additions & 0 deletions docs/code-flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Per-service code-flow walkthroughs

Five walkthroughs — one per microservice — that show **which files, classes, and interfaces get touched along the most-load-bearing request path** in each service. New contributors should read the walkthrough for whichever service they're about to touch before opening a file.

Each walkthrough is the same shape:
- Short intro framing the service's role + architecture style
- Mermaid `sequenceDiagram`(s) for the main flow(s), with lane labels = `ClassName<br/>file/path.cs`
- `stateDiagram-v2` if the service has a state machine
- `graph LR` showing structural relationships (CQRS split, DI wiring) when it adds clarity
- File inventory table
- Cross-references to neighbours

---

## The 5 services

| Service | Pattern | Role in the saga | Walkthrough |
|---|---|---|---|
| **OrderService** | VSA | Saga **entry point** + state owner (Order aggregate) | [orderservice.md](code-flows/orderservice.md) |
| **CatalogService** | Clean Architecture | Product catalog (read-heavy, cached); gRPC server for synchronous validation from Order | [catalogservice.md](code-flows/catalogservice.md) |
| **PaymentService** | VSA | Saga **middle** — charges via gateway + publishes outcome; recovery sweeper for stuck Pendings | [paymentservice.md](code-flows/paymentservice.md) |
| **ShippingService** | VSA | Saga **last** — creates & dispatches shipment; buyer-scoped read with IDOR protection | [shippingservice.md](code-flows/shippingservice.md) |
| **NotificationService** | VSA (minimal — no `Domain/`) | Stateless event-to-email pump (3 events in, email out) | [notificationservice.md](code-flows/notificationservice.md) |

---

## The saga at a glance — how the 5 services connect

Each walkthrough explains its own service in depth. This diagram is the glue — what events flow where, in time order.

```mermaid
sequenceDiagram
autonumber
actor Buyer
participant Order as OrderService
participant Cat as CatalogService
participant Pay as PaymentService
participant Ship as ShippingService
participant Notif as NotificationService

Buyer->>Order: POST /api/v1/orders
par sync gRPC (validate + reserve stock)
Order->>Cat: GetProductAsync × N
Cat-->>Order: ProductDto × N
Order->>Cat: ReserveStockAsync × N
Cat-->>Order: bool × N
end

Order->>Order: persist Order + stage<br/>OrderPlacedEvent in outbox<br/>(same DB tx)
Order-->>Buyer: 202 Accepted

Note over Order,Notif: From here on, async over Azure Service Bus
Order--)Pay: OrderPlacedEvent
Order--)Notif: OrderPlacedEvent (parallel)

Pay->>Pay: ProcessPaymentHandler<br/>(idempotency check, gateway call,<br/>state transition, publish)

alt gateway success
Pay--)Order: PaymentCompletedEvent
Pay--)Ship: PaymentCompletedEvent (parallel)
Order->>Order: PaymentCompletedHandler<br/>MarkAsPaid()

Ship->>Ship: CreateShipmentHandler<br/>(idempotency, create, dispatch)
Ship--)Order: ShipmentDispatchedEvent
Ship--)Notif: ShipmentDispatchedEvent (parallel)
Order->>Order: ShipmentDispatchedHandler<br/>MarkAsShipped()

Notif->>Notif: SendNotification ("Order Shipped")
else gateway failure
Pay--)Order: PaymentFailedEvent
Pay--)Notif: PaymentFailedEvent (parallel)
Order->>Order: PaymentFailedHandler<br/>MarkAsPaymentFailed()
Notif->>Notif: SendNotification ("Payment Failed")
end
```

The dashed (`--)`) arrows are over Service Bus; the solid arrows inside Phase 1 are synchronous HTTP/gRPC.

---

## Conventions across all 5 walkthroughs

**Lane labels in sequence diagrams** use `ClassName<br/>file/path.cs` so you can read a flow without checking a separate legend.

**Color coding inside `graph LR` blocks** (where used):
- Blue (`#dbeafe`) — write loaders / mutable state
- Green (`#a7f3d0`) — read projections / success endpoints
- Orange (`#fed7aa`) — start/trigger / infrastructure
- Purple (`#ddd6fe`) — caching layer

**Mermaid syntax gotcha** worth knowing if you edit these files: inside a `Note over X: <content>` block, no `;` or `:` allowed in the content — Mermaid uses both as parser delimiters. Use em-dash (`—`) or comma instead.

---

## See also

- [docs/architecture.md](architecture.md) — system-level architecture (Aspire, transport, polyglot persistence)
- [docs/cqrs-data-access.md](cqrs-data-access.md) — the read/write split rule that shapes each service's repository layer
- [docs/event-catalog.md](event-catalog.md) — every event's shape, producer, and consumer (the saga's contract surface)
- [docs/transactional-outbox.svg](transactional-outbox.svg) — diagram of outbox mechanics referenced from multiple walkthroughs
- [docs/hybridcache-flow.svg](hybridcache-flow.svg) — diagram of CatalogService's L1+L2 cache + stampede protection
- [docs/nextaurora-architecture.svg](nextaurora-architecture.svg) — the full-system visual referenced in the saga-at-a-glance section above
- [CLAUDE.md](../CLAUDE.md) — the canonical rules every walkthrough references
Loading
Loading