Skip to content

⭐ Notification Service

Terrence Daniels edited this page Sep 15, 2026 · 2 revisions

This repo's first polyglot service — Node.js/TypeScript, not .NET — consuming billing.invoice.generated and delivering a real outbound webhook POST per invoice. Demonstrates a consistent event contract holding across a language boundary, not just within one runtime.

Composition

  • notificationConsumer — a kafkajs consumer (@kafkajs/confluent-schema-registry for Avro decoding) subscribed to billing.invoice.generated
  • accountClient — calls Account/Customer Service's GET /accounts/{accountId} to resolve where this invoice's webhook should go
  • webhookSender — a real outbound fetch POST per invoice, JSON body { invoiceId, accountId, amountDue, dueDate }
  • Hosted via Aspire.Hosting.JavaScript 13.5.3's AddNodeApp(...).WithYarn().WithRunScript("start") — the first non-.NET resource this AppHost orchestrates
  • First real use of this repo's Yarn Berry workspace + catalog: protocol, both scaffolded back at repo bootstrap but unused until now

Real bug found and fixed — a missing runtime dependency

yarn install warned that "notification-service doesn't provide vite, requested by vitest" — looked like routine peer-dependency noise. It wasn't: vitest 5.0.0 requires vite as a real dependency, and without it yarn test failed outright with ERR_MODULE_NOT_FOUND before running a single test. Fixed by adding vite to the shared catalog and this service's devDependencies — issue #23.

Real bug found and fixed — a poison message crashing the consumer permanently

Live verification found the consumer group sitting at state: EMPTY, 0 members, after running for several minutes with no webhooks delivered. Root cause: @kafkajs/confluent-schema-registry's decode() decodes strictly against the writer's registered schema, with no reader-schema projection — unlike the C# Confluent.SchemaRegistry.Serdes.Avro deserializer everywhere else in this repo. billing.invoice.generated is a long-lived dev topic still holding messages written before the AccountId re-keying; decoding one gave accountId: undefined rather than the schema's declared "" default, which threw, which kafkajs retried forever on the same offset until the consumer crashed out of its group entirely — a poison-message problem, not just stale test data. Fixed by skipping messages with a missing/falsy accountId instead of letting the exception propagate. Re-verified live: skipped ~4,387 old incompatible messages, then delivered real webhooks for genuinely new invoices — issue #24.

Real bug found and fixed — a delivery failure crashing the consumer too

The same crash pattern as #24, found from a different trigger while testing unrelated dev tooling: getAccountWebhookUrl and sendInvoiceNotification can both throw for ordinary reasons (a network error, DNS failure, connection refused, a non-2xx response), and neither was caught — one unreachable webhook target crashed the whole consumer, blocking every other account's notifications along with it. Fixed by wrapping the account-lookup + webhook-send step in its own try/catch: log and move on, since webhook delivery here is best-effort, not guaranteed at-least-once. Re-verified live: ran the full stack with no webhook listener running at all — every single delivery attempt failed, and the consumer group stayed STABLE with 0 lag, fully caught up to the latest offset instead of crashing — issue #25.

Tested where it's worth it, verified live where it isn't

7 Vitest tests cover the pure logic — accountClient, webhookSender, config — with a mocked fetch. notificationConsumer's kafkajs orchestration itself is deliberately left to live verification rather than mocking kafkajs's consumer plumbing, the same "unit test the logic, prove the wiring live" split used for AccountCustomer.Tests' REST endpoints.

Verified live, not just built

Ran the real AppHost with a temporary local HTTP listener standing in for the configured webhook URL. Confirmed real webhook POSTs actually received, correct JSON shape, accountId a real account GUID — cross-checked against matching Invoice rows in Postgres via psql.