Background jobs and caching
Skill kennguyen887/agent-foundation/skills/background-jobs-and-caching
Claude Code skills marketplace — backend & frontend engineering conventions + step-by-step third-party integration recipes: Stripe, Rapyd, CyberSource, UOB & wallet payments, Singpass/Keycloak OIDC & 3-D Secure, Twilio SMS, Docker & CI/CD. NestJS/TypeScript + React, language-flexible.
npx -y skills add kennguyen887/agent-foundation --skill background-jobs-and-cachingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when adding background jobs (Bull queues) or a Redis cache to a backend service — multi-queue architecture, enqueue/process, dynamic delayed jobs, job idempotency via a DB lock, graceful shutdown, and Redis caching (read-through wrap, key conventions, event-driven + prefix-SCAN invalidation). NestJS/TypeORM reference, framework-flexible. Complements (not replaces) the SQS cross-service events pattern.
SKILL.md
6.2 KB, as published. Nobody here has run it
Background jobs & caching
In-process async work (Bull + Redis) and a Redis cache for a backend service. Examples NestJS/TS,
neutral listing/payment domain. principle → ▸ Example → ▸ Other stacks. Cross-service
async (vs this in-process Bull) → the SQS events pattern in write-service-code §6.
When to use
Time-critical, in-process async (reminders, expiry/timeout actions, retries, fan-out) and caching hot reads. Bull (this skill) = in-process, Redis-backed, supports delays + per-queue retry; SQS = cross-service. Complementary — don't merge them.
1. Bull job queues
- One named queue per job type, not a single mega-queue — independent concurrency + retry:
Set default job options centrally:BullModule.registerQueue({ name: REMINDER_QUEUE }, { name: PAYMENT_EXPIRY_QUEUE });removeOnComplete: true,removeOnFail: { age: <1h>, count: <N> }(so Redis doesn't fill with finished jobs), plus a retry policy —attempts: <n>withbackoff: { type: 'exponential', delay: <ms> }— so a transient failure retries with growing delay instead of dying on the first error or hammering the dependency instantly. - Producer injects the queue; processor handles it:
@InjectQueue(PAYMENT_EXPIRY_QUEUE) private queue: Queue; await this.queue.add(JOB.expirePayment, { paymentId }, opts); @Processor(PAYMENT_EXPIRY_QUEUE) class PaymentExpiryProcessor { @Process(JOB.expirePayment) async handle(job: Job) { await this.commandBus.execute(new ExpirePaymentCommand(job.data)); } } - Dynamic delays from business time (remind N hours before, expiry windows) — compute with the
date lib, clamp ≥ 0:
const delay = Math.max(dayjs(startTime).subtract(2, 'hour').diff(dayjs()), 0); await this.reminderQueue.add(JOB.remind, { id }, { delay }); - Idempotency via a DB lock — at-least-once delivery means a job can run twice; guard with a
unique-key insert (a
locking_recordstable), skip on conflict:
Alternative — a Redis lock for short-window/HTTP dedup:async runOnce(jobId: string, execute: () => Promise<void>) { try { await this.lockRepo.insert({ id: jobId }); } // PK/unique → throws if already seen catch { return; } // already processed → skip await execute(); }SET lock:<key> 1 PX <ttl> NXsucceeds only if the key is absent; anullreply means a duplicate is already in flight → skip (fail-safe: treat errors as "locked"). Lighter than a DB row for idempotency-key/endpoint dedup; the DB-row lock is better for a permanent once-only guarantee. - Graceful shutdown — drain in-flight jobs on deploy:
export class JobQueueModule implements OnApplicationShutdown { async onApplicationShutdown() { await this.queue.close(); } // wait for active jobs } - No cron lib for data-driven timing — model "do X at time T" as a delayed job, not a cron
sweep (jobs persist in Redis, survive restarts). Use a scheduler only for fixed wall-clock tasks —
and guard a recurring/cron job against overlap (a slow run must not double-fire) with the same
Redis
SET NXlock: skip the run if the lock is already held. ▸ Other stacks: any job lib (BullMQ, Sidekiq, Celery, River) — same shape: named queues, delayed jobs, idempotent handlers, drain on shutdown.
2. Redis caching
- One
CacheServicewraps the cache client; read-through withwrap:cache.wrap(key, () => fetchExpensive(), ttlSeconds); // get-or-compute-and-store - Key conventions: a central
CACHE_PREFIXregistry (no scattered string literals); composite keys must serialize stably —`${PREFIX.LISTING}:${stableStringify(query)}`(unsorted object keys → different strings → silent cache misses). - Invalidate on write, in the handler/event (not scattered in services): after a mutation, delete
the affected keys — and all variants (v1/v2) — with
Promise.all:await Promise.all([ cache.del(`${PREFIX.LISTING_DETAIL}:${id}`), cache.del(`${PREFIX.LISTING_DETAIL_V2}:${id}`), ]); - Bulk invalidate by prefix with SCAN, never
KEYS(KEYSblocks Redis): streamscanStream({ match: 'PREFIX:*' })→pipeline.unlink(...). - TTL from config (a default + per-key override); don't hard-code. ▸ Other stacks: any cache (Redis/Memcached) — read-through wrapper, prefixed + stably-serialized keys, invalidate-on-write, SCAN-not-KEYS for bulk.
Verification
- One queue per job type, finished jobs evicted:
redis-cli KEYS "bull:*"shows a key set per queue (not one mega-queue); after a job completesredis-cli LLEN bull:<queue>:completedstays ~0 (removeOnComplete).grep -rn "@InjectQueue\|@Process\|registerQueue" src— producers/processors paired per queue; noadd(..., { delay: <negative> })(clamped viaMath.max(…, 0)). - Idempotent + drains: enqueue the same
jobIdtwice → the side effect runs once (SELECT count(*) FROM locking_records WHERE id='<jobId>'= 1).grep -rn "runOnce\|OnApplicationShutdown" srcpresent; SIGTERM with a job in flight → process waits,redis-cli LLEN bull:<queue>:activereaches 0 before exit. - Cache discipline: reads go through
cache.wrap(grep -rn "\.wrap(" src); keys come from the prefix registry, no raw literals; anti-pattern checkgrep -rn "\.keys(" src→ empty (bulk invalidation must usescanStream, neverKEYS). After a mutation,redis-cli GET "<PREFIX>:<id>"→nil(invalidated, all variants).
Related
write-service-code— §6 (SQS cross-service events; complementary) + Robustness (transactions/event handlers).database-migrations(the locking-records table is a migration) ·code-conventions.