This file provides guidance to Claude Code when working with code in this repository.
go test ./... # Run all tests
go test -run TestName # Run a specific test
go build ./... # Build all packages
go mod tidy # Clean up dependenciesDead Letter Queue (DLQ) package for the github.com/rbaliyan/event event pub-sub library. Provides storage, retrieval, replay, and cleanup of messages that failed processing after exhausting retries.
Store Interface (store.go) - Defines the contract for DLQ persistence:
Store(msg)- Add a failed messageGet(id)- Retrieve a message by IDGetByOriginalID(originalID)- Retrieve a message by the original event message ID (ErrNotFoundif absent)List(filter)- Query messages with filteringCount(filter)- Count matching messagesMarkRetried(id)- Mark message as replayedDelete(id)- Remove a messageDeleteOlderThan(age)- Cleanup old messagesDeleteByFilter(filter)- Bulk delete matching messages
Manager (manager.go) - High-level API for DLQ operations:
- Wraps a Store and Transport
Store()- Creates DLQ message with auto-generated IDReplay()- Republishes messages to original eventsReplaySingle()- Replay a single message by IDCleanup()- Remove old messagesStats()- Get DLQ statistics
StoreAdapter (adapter.go) - Bridges a Store to the event bus. NewStoreAdapter(store, source) returns an adapter implementing the event.DLQStore interface; pass it to event.WithDLQ(...) so the bus auto-captures messages that exhaust their retries (generating the DLQ ID and tagging each with source). This is the primary production wiring path — see examples/main.go.
Store Implementations:
MemoryStore(memory.go) - Thread-safe in-memory store for testingPostgresStore(postgres.go) - PostgreSQL with JSONB metadataMongoStore(mongodb.go) - MongoDB with capped collection supportRedisStore(redis.go) - Redis Streams + Hashes
// Message in the DLQ
type Message struct {
ID string // DLQ message ID (generated)
EventName string // Original event name
OriginalID string // Original message ID
Payload []byte // Message payload
Metadata map[string]string // Message metadata
Error string // Failure error
RetryCount int // Retries attempted (or re-DLQ count when dedup enabled)
CreatedAt time.Time // When added to DLQ (first-seen when dedup enabled)
RetriedAt *time.Time // When replayed (nil if never)
QuarantinedAt *time.Time // When Replay classified as terminal (nil if not quarantined)
Source string // Source service
}
// Filter for querying messages
type Filter struct {
EventName string
After time.Time // messages received after this time
Before time.Time // messages received before this time
Error string // Contains match
MaxRetries int
Source string
ExcludeRetried bool
ExcludeQuarantined bool // Exclude terminal/quarantined messages
Limit int
Offset int
}
// Statistics
type Stats struct {
TotalMessages int64
PendingMessages int64 // Excludes quarantined messages
RetriedMessages int64
QuarantinedMessages int64 // Messages quarantined as terminal
MessagesByEvent map[string]int64
MessagesByError map[string]int64
OldestMessage *time.Time
NewestMessage *time.Time
}Interface-Based Extensibility: Store interface allows custom implementations
Optional Interface: StatsProvider for stores that support detailed statistics
Optional Interface: Quarantiner for stores that support terminal-message quarantine — Quarantine(ctx, id) error; detected via type assertion (same pattern as StatsProvider); all four built-in stores implement it
Functional Options: Store constructors take variadic options and (except NewMemoryStore) return an error:
store, err := dlq.NewMongoStore(db, dlq.WithCollection("custom_dlq"))Replay Metadata: Replayed messages include DLQ metadata:
dlq_replay: "true"dlq_message_id: DLQ IDdlq_original_error: original failure
Terminal-error quarantine: WithTerminalError(func(*Message) bool) ManagerOption — default nil (no behaviour change). When set, Replay/ReplaySingle quarantine matching messages instead of republishing them. TerminalErrorMatching(patterns ...string) is a helper that does case-sensitive substring match on Message.Error (empty error never matches). Replay always forces Filter{ExcludeQuarantined:true} internally.
Max replay attempts (error-agnostic loop cap): WithMaxReplayAttempts(n int) ManagerOption — default 0 (unlimited, prior behaviour). When n>0, Replay/ReplaySingle quarantine a message once it has been replayed n times, regardless of why it fails. The count rides in message metadata under MetadataReplayCount (dlq_replay_count) and is incremented by replayMessage on each republish, so it accumulates across the republish→re-DLQ cycle without requiring dedup or a stable row. This is the generic backstop against a permanently-failing message (decode error, future type change, bad payload) looping forever; prefer it over WithTerminalError for guaranteed termination. Quarantine reason is logged/distinguished as max_replay_attempts vs terminal_error.
Opt-in deduplication: OFF by default — every Store call inserts a distinct row. Enable per backend: WithMemoryDedup(), WithMongoDedup(), WithPostgresDedup(), WithRedisDedup(). When enabled, re-storing a message with the same non-empty (EventName, OriginalID) upserts: increments retry_count, updates error/payload/metadata, preserves created_at, clears retried_at. Empty OriginalID always inserts distinct.
MigrateDedup: MigrateDedup(ctx, ...MigrateDedupOption) (int64, error) on Mongo/Postgres/Redis stores. Collapses existing duplicates (keeps newest, sums retry_count, keeps oldest created_at, OR-s quarantine), then creates the unique index. Refuses to delete >50% of rows unless WithForce() is passed. Run before enabling dedup on a table/collection that already has data.
github.com/rbaliyan/event/v3- Event library (transport, message types)github.com/google/uuid- Message ID generationgithub.com/redis/go-redis/v9- Redis clientgo.mongodb.org/mongo-driver- MongoDB drivergo.opentelemetry.io/otel/trace- Span context for replay
Tests use MemoryStore and mock transport. Run with:
go test -v ./...See examples/main.go for a complete at-least-once delivery setup with:
- Transactional outbox
- Resume tokens
- Ack store
- DLQ integration
- Worker groups