plugins/backend-toolkit/skills/async-messaging/SKILL.md
Build reliable event-driven flows with the Transactional Outbox pattern — write state and event in one transaction, relay asynchronously, achieve at-least-once delivery + consumer idempotency. Use when an action must reliably trigger downstream work, or when events are lost on crash (dual-write problem). Not for simple background work without state+event reliability (use background-jobs) or outbound HTTP webhook specifics (use webhook-design).
npx skillsauth add jaykim88/claude-ai-engineering async-messagingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Make "change state AND notify the world" reliable. The naive approach (write DB, then publish to a queue) loses events on crash between the two steps — the dual-write problem. The Outbox pattern solves it.
Universal — the Outbox pattern, the dual-write problem, and "exactly-once = at-least-once + consumer idempotency" are distributed-systems fundamentals independent of broker/language.
Recognize the dual-write problem
Apply the Transactional Outbox
outbox row in ONE local DB transaction (see transaction-management)Relay the outbox asynchronously
background-jobs task reads unsent outbox rows, publishes to the broker, marks sentDesign consumers for at-least-once (idempotency)
resilience-patterns)4b. Version events from day one (additive-only)
eventType + eventVersion (or schemaVersion) on every payload — consumers will outlive any one publisherapi-contract)eventType + version4c. Dead-letter queue (DLQ) + poison-message handling
resilience-patterns / background-jobs)order_id, user_id). Within a partition: ordered; across partitions: no guarantees. Choose the key by the invariant you actually need5b. Retain only what you need
| ❌ Anti-pattern | ✅ Correct |
|---|---|
| Write DB then publish to broker (dual write) | Write state + outbox row in one transaction, relay async |
| Assuming the broker gives exactly-once | At-least-once + idempotent consumers |
| Publishing inside the business transaction (network I/O in tx) | Outbox row in tx; relay outside |
| Heavyweight log broker for a simple in-app job | Simple queue first; log broker only at real scale |
| Consumer with no dedup | Dedupe on event id |
| Two payload shapes under the same eventType (silent break) | eventVersion + additive-only changes; run old+new in parallel on cutover |
| Poison messages blocking the queue indefinitely | DLQ after retry budget; alert on DLQ growth; manual replay |
| Outbox table growing unbounded | Scheduled cleanup of sent rows past retention window |
| Ordering assumed without a partition key | Choose a partition key matching the invariant (same-entity events to the same partition) |
| Tier | Examples | Action SLA |
|---|---|---|
| Critical | Dual-write losing payment/order events on crash; consumer with no idempotency double-charging; breaking schema change shipped on a live eventType (consumers crash on next message) | Fix immediately |
| Major | Publishing inside a transaction (lock contention); no DLQ for poison messages; outbox table grows without cleanup (DB-eating); no partition key on a flow that requires ordering | Fix this sprint |
| Minor | Polling interval untuned; broker over-provisioned (Kafka for tiny load); broker retention not aligned with actual replay needs | Schedule within 2 sprints |
eventVersion; schema changes are additive-onlyfeat(events): add outbox for <event> / feat(events): idempotent consumer for <event>outbox(id, type, payload, created_at, sent_at) written inside prisma.$transaction with the state changeWHERE sent_at IS NULL, publishes, sets sent_attransaction-management — the outbox row is written in the same transaction as the state changebackground-jobs — the outbox relay / consumers run as jobswebhook-design — outbound webhooks can be driven by the outboxdevelopment
Design webhooks correctly on both sides — sending (HMAC signing, retries with backoff, at-least-once) and receiving (verify signature on raw body, enqueue + 200 fast, dedupe on event id). Use when adding webhook delivery or consuming a provider's webhooks. Not for internal service-to-service events (use async-messaging) or general outbound-call retry policy (use resilience-patterns).
testing
Use transactions and isolation levels correctly — keep them short, no network calls inside, explicit isolation, retry on serialization conflicts, and choose optimistic vs pessimistic locking. Use when a write spans multiple tables, when concurrent updates corrupt data, or when designing money/inventory flows. Not for cross-service event delivery (use async-messaging Outbox) or schema-level constraints (use schema-design).
development
Backend testing pyramid — unit for pure logic, integration against a real DB (Testcontainers), and consumer-driven contract testing (Pact) for service boundaries. Use before a feature, after a bug fix, or when services break each other on deploy. Not for load testing (use performance-profiling) or security testing (use backend-security-audit).
data-ai
Design a relational schema — normalize to 3NF then denormalize with justification, choose the right Postgres index type per data shape, enforce constraints at the DB. Use when modeling a new domain, when queries are slow, or before a migration. Not for diagnosing slow queries (use query-optimization) or shipping the change without downtime (use migration-strategy).