diff --git a/CLAUDE.md b/CLAUDE.md index 44a578cc..a2e2459d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -171,6 +171,7 @@ This rule is for humans, AI assistants, and future-you. Don't wait to be asked. ## Communication Patterns - **Async events** (Azure Service Bus): For workflow orchestration (order -> payment -> shipping -> notification) +- **Durability ≠ replay — don't reach for a stream just to avoid losing messages.** A common misconception is "pub/sub loses the message if a subscriber is down, so use a stream (Kafka/Event Hubs) when you can't afford loss." Not so: message loss depends on whether the subscription is **durable**, not on queue-vs-stream. The misconception comes from **Redis Pub/Sub specifically**, which *is* fire-and-forget — no persistence, a subscriber that's down when a message publishes misses it forever; there, the user's intuition is right and Redis **Streams** (persistent, consumer-group offsets, replayable) is the durable answer *within the Redis ecosystem*. But that's a property of Redis Pub/Sub being non-durable, not of pub/sub as a pattern: **durable** pub/sub (Azure Service Bus topics+subscriptions, RabbitMQ durable queues, AWS SNS→SQS) does NOT lose messages — the broker persists per-subscriber until ack. NextAurora's stack already can't lose a message on either side — the **transactional outbox** guards the publish side (event persisted in the same DB transaction as the entity write, dispatched with retry, so "entity saved but event lost" can't happen), and **durable Service Bus subscriptions / RabbitMQ durable queues** guard the consume side (the broker holds the message per-subscriber until ack, so a down service resumes from where it left off). At-least-once delivery means the real risk is *duplication, not loss* — which is why every handler is idempotent (see "Key Conventions: Event handlers must be idempotent"). **Reach for a stream (Kafka, Azure Event Hubs, Redis Streams) only when you need what a durable queue can't give: replay from an offset, multi-day retention, an ordered append-only event log, or N independent consumers each re-reading history at their own pace** — *not* merely "don't lose messages." NextAurora has no such need today (it deliberately deleted the hand-rolled `EventLogs` replay table; any future replay rides Wolverine's own message store). Adding a stream to prevent loss the outbox + durable-queue + idempotency stack already prevents is the same speculative over-engineering the factory-pattern rule warns against. Full transport-selection decision guide (Redis Pub/Sub vs Streams vs RabbitMQ vs ASB vs SNS+SQS vs Kafka/Event Hubs — portable across systems): [docs/messaging-transport-selection.md](docs/messaging-transport-selection.md). - **gRPC** (sync): For real-time queries between services (OrderService -> CatalogService product validation). gRPC is versioned separately via `.proto` `package` declarations. - **REST** (HTTP): For frontend-to-service communication only. URL-segment versioned via `Asp.Versioning.Http` — every endpoint lives under `/api/v{version}/...`. Default version is `1.0`; the version segment is required (`AssumeDefaultVersionWhenUnspecified = false`). **Always use `app.MapV1ApiGroup("Tag", "resource")`** (helper in `NextAurora.ServiceDefaults`) to register a versioned route group — it returns a `RouteGroupBuilder` rooted at `/api/v1/resource` and applies the version + tag in one call. Don't hand-roll `NewVersionedApi(...).MapGroup(...).HasApiVersion(...)` chains — drift across services is the failure mode. To add v2 later, register a side-by-side group with `.HasApiVersion(new ApiVersion(2, 0))`; v1 keeps working untouched. - **Wolverine handler discovery is NOT DI registration — two separate containers.** `opts.Discovery.IncludeAssembly(...)` builds Wolverine's *internal* handler-type map (message-type → handler-type) used by `IMessageBus.InvokeAsync()` / `PublishAsync()`. Wolverine constructs handlers itself via `IServiceScopeFactory` — it never asks `IServiceCollection` for the handler type. Therefore: **`serviceProvider.GetRequiredService()` throws `InvalidOperationException` unless you also `AddScoped()`.** Production code paths go through `IMessageBus` and never hit this; the path that does hit it is **integration tests that resolve handlers directly to assert the EF projection SQL** (read-handler integration tests, by far the most common case). Rule: any handler resolved by `GetRequiredService()` in tests must have an explicit `services.AddScoped()` in that service's `AddXInfrastructure` registration. Reference: [OrderService/Infrastructure/DependencyInjection.cs](OrderService/Infrastructure/DependencyInjection.cs) (the `AddScoped()` / `AddScoped()` pair). Failure mode that surfaced this gap: `OrderReadProjectionTests` failed in CI with `No service for type 'OrderService.Features.GetOrderByIdHandler' has been registered` after the repository-wrapper refactor — pre-refactor the tests resolved `IOrderRepository` (registered as `AddScoped()`), and the conversion to handler-resolved tests missed the equivalent registration.