8.5 KiB
Allowance Scheduling System (Backend)
This document captures the complete allowance scheduling feature as implemented in the backend. It is intended as a long-term reference for how the system works, why it was built this way, and how to operate/extend it safely.
Goals and Scope
- Allow parents/guardians to create a recurring allowance schedule for a child.
- Credit the allowance automatically based on the schedule.
- Scale safely with multiple workers and avoid duplicate credits.
- Keep cron lightweight and offload work to RabbitMQ workers.
Non-goals in the current implementation:
- UI changes
- Analytics/reporting views
- Advanced scheduling (e.g., custom weekdays)
High-Level Flow
- Parent creates a schedule via API.
- Cron runs every 5 minutes and enqueues due schedules into RabbitMQ.
- Workers consume queue messages, credit the allowance, and update the next run.
- Idempotency is enforced in the database to prevent duplicate credits.
Data Model
Table: allowance_schedules
Purpose: Stores the schedule definition (amount, frequency, status, next run).
Key fields:
guardian_id,junior_id: who funds and who receives.amount: the allowance amount.frequency: DAILY / WEEKLY / MONTHLY.status: ON / OFF.next_run_at,last_run_at: scheduling metadata.
Constraints:
- Unique
(guardian_id, junior_id)ensures one schedule per child.
Entity: src/allowance/entities/allowance-schedule.entity.ts
Table: allowance_credits
Purpose: Audit log and idempotency guard for each credit run.
Key fields:
schedule_id: which schedule executed.transaction_id: the resulting transaction (nullable).amount,run_at,credited_at.
Idempotency:
- Unique
(schedule_id, run_at)prevents duplicates even with multiple workers.
Entity: src/allowance/entities/allowance-credit.entity.ts
Table: cron_runs
Purpose: shared audit table for all cron jobs (not just allowance).
Key fields:
job_name: unique identifier for the cron.status: SUCCESS / FAILED.processed_count: number of schedules processed.error_message: failure reason.started_at,finished_at.
Entity: src/cron/entities/cron-run.entity.ts
API (Schedule Creation)
Endpoint:
POST /guardians/me/allowances/:juniorId
Input DTO:
amount(required, numeric, positive)frequency(required, enum)status(required, enum)
DTO: src/allowance/dtos/request/create-allowance-schedule.request.dto.ts
Response DTO:
AllowanceScheduleResponseDtowith schedule fields.
DTO: src/allowance/dtos/response/allowance-schedule.response.dto.ts
Business validation:
- Child must belong to guardian.
- No duplicate schedule for the same guardian+child.
Service: src/allowance/services/allowance.service.ts
Cron Producer (Queue Enqueue)
Cron job:
- Runs every 5 minutes.
- Batches schedules (cursor-based) to avoid large load.
- Enqueues each schedule to RabbitMQ.
Cron: src/cron/tasks/allowance-schedule.cron.ts
Batch behavior:
- Batch size = 100
- Uses a cursor (
nextRunAt,id) for stable pagination. - Prevents re-reading the same rows.
Locking:
BaseCronServiceuses cache lock to ensure only one instance runs.- Each cron run is logged to
cron_runswith status and processed count.
Lock: src/cron/services/base-cron.service.ts
Queue Publisher
Queue publisher:
- Asserts queue + retry/DLQ exchanges.
- Enqueues jobs with message id (for traceability).
Service: src/allowance/services/allowance-queue.service.ts
Queue names:
allowance.schedule(main)allowance.schedule.retry(retry with TTL)allowance.schedule.dlq(dead-letter queue)
Constants: src/allowance/constants/allowance-queue.constants.ts
Worker Consumer (Transfers)
Worker:
- Consumes
allowance.schedulequeue. - Validates schedule is due and active.
- Creates
allowance_creditsrecord for idempotency. - Transfers money via
cardService.transferToChild. - Updates
last_run_atandnext_run_at.
Worker: src/allowance/services/allowance-worker.service.ts
Transfer:
- Uses existing logic in
card.service.tsfor balance updates and transaction creation.
Service: src/card/services/card.service.ts
Idempotency details
- Worker inserts
allowance_creditsrow first. - Unique constraint blocks duplicates.
- If transfer fails, the credit row is removed so the job can retry.
This makes multiple workers safe.
Retry + DLQ Strategy
Retry delay:
- Failed jobs are dead-lettered to retry queue.
- Retry queue has
messageTtl = 10 minutes. - After TTL, job is routed back to main queue.
DLQ:
- If a job fails
ALLOWANCE_MAX_RETRIEStimes (default 5), it is routed to the DLQ. - This prevents endless loops and allows manual inspection.
Config:
ALLOWANCE_MAX_RETRIES(default 5)
Redis Usage
Redis is used by BaseCronService to enforce a distributed lock for the cron job.
This prevents multiple backend instances from enqueuing the same schedules at the same time.
Lock behavior:
- If the lock key exists, cron exits early.
- If the lock key is absent, cron sets it with a TTL and proceeds.
- TTL ensures the lock is released even if a node crashes.
Service: src/cron/services/base-cron.service.ts
RabbitMQ Setup and Behavior
The allowance system uses RabbitMQ for asynchronous processing. Cron publishes due schedules, and workers consume them.
Exchanges and Queues
Main queue:
allowance.schedule
Retry setup:
- Exchange:
allowance.schedule.retry.exchange - Queue:
allowance.schedule.retry - Binding key:
allowance.schedule - TTL: 10 minutes
- Dead-letter route: back to
allowance.schedule
Dead-letter queue (DLQ):
- Exchange:
allowance.schedule.dlq.exchange - Queue:
allowance.schedule.dlq - Binding key:
allowance.schedule
Flow Summary
- Cron enqueues jobs to
allowance.schedule. - Worker consumes jobs from
allowance.schedule. - On failure, job is dead-lettered to
allowance.schedule.retry. - After 10 minutes, it returns to
allowance.schedule. - After max retries, worker publishes the job to
allowance.schedule.dlq.
Where it is configured
- Publisher setup:
src/allowance/services/allowance-queue.service.ts - Worker consumer:
src/allowance/services/allowance-worker.service.ts - Queue constants:
src/allowance/constants/allowance-queue.constants.ts
Environment Variables
RABBITMQ_URL(required for queue/worker)ALLOWANCE_QUEUE_NAME(optional, defaults toallowance.schedule)ALLOWANCE_MAX_RETRIES(optional, defaults to 5)ALLOWANCE_RETRY_DELAY_MS(optional, defaults to 10 minutes)
Example .env snippet
RABBITMQ_URL=amqp://guest:guest@localhost:5672
ALLOWANCE_QUEUE_NAME=allowance.schedule
ALLOWANCE_MAX_RETRIES=5
ALLOWANCE_RETRY_DELAY_MS=600000
Operational Checklist
- Ensure Redis is running (cron locking).
- Ensure RabbitMQ is running (queue + workers).
- Start at least one worker process.
- Monitor DLQ for failures.
Manual Test Checklist
- Create schedule:
- POST
/guardians/me/allowances/:juniorId - Valid amount, frequency, status.
- POST
- Duplicate schedule:
- Expect
ALLOWANCE.ALREADY_EXISTS.
- Expect
- Cron enqueue:
- Wait for cron interval or manually trigger.
- Confirm messages appear in RabbitMQ.
- Worker:
- Ensure worker is running.
- Verify transfers happen and
allowance_creditsis created.
- Failure paths:
- Simulate transfer failure and verify retry queue behavior.
- Confirm DLQ after max retries.
Operational Notes
- For large volumes, scale workers horizontally.
- Keep cron lightweight; do not perform transfers in cron.
- Monitor queue depth and DLQ entries.
Known Limitations (Current)
- Only one schedule per child (guardian+junior unique).
- No custom weekdays or complex schedules.
- Retry delay is fixed at 10 minutes (can be configurable).
File Map (Quick Reference)
-
API:
src/allowance/controllers/allowance.controller.tssrc/allowance/services/allowance.service.tssrc/allowance/dtos/request/create-allowance-schedule.request.dto.tssrc/allowance/dtos/response/allowance-schedule.response.dto.ts
-
Cron:
src/cron/tasks/allowance-schedule.cron.tssrc/cron/services/base-cron.service.ts
-
Queue/Worker:
src/allowance/services/allowance-queue.service.tssrc/allowance/services/allowance-worker.service.tssrc/allowance/constants/allowance-queue.constants.ts
-
Repositories:
src/allowance/repositories/allowance-schedule.repository.tssrc/allowance/repositories/allowance-credit.repository.ts
-
Entities:
src/allowance/entities/allowance-schedule.entity.tssrc/allowance/entities/allowance-credit.entity.ts