Skip to main content

Background Jobs, Testing & Production Operations

Reliability and operational visibility are essential for financial infrastructure. This guide covers how asynchronous background processing, testing suites, real-time diagnostics, and GitOps deployments function across Roja.


Asynq Background Task Processing

Roja processes all asynchronous, deferred, and scheduled tasks using Asynq backed by Redis 8.

┌───────────────────────┐
│ API SERVER │
│ (Business Operation) │
└───────────┬───────────┘
│ 1. Atomic DB Write

┌───────────────────────┐
│ OUTBOX EVENTS │ (Postgres Table `outbox_events`)
└───────────┬───────────┘
│ 2. Leases & Publishes (At-least-once)

┌───────────────────────┐
│ REDIS 8 │
│ (15 Weighted Queues) │
└───────────┬───────────┘
│ 3. Distributes Tasks by Queue Weight

┌───────────────────────┐
│ ASYNQ WORKER │ (backend/cmd/asynq-worker)
│ • Payment Settlement │
│ • Webhook Dispatch │
│ • Push Notifications │
└───────────────────────┘

Worker Daemons & Binaries

Binary / CommandSource PathResponsibility
asynq-workerbackend/cmd/asynq-workerPrimary consumer daemon executing async task handlers across all queues.
asynq-schedulerbackend/cmd/asynq-schedulerEmits periodic cron tasks: daily direct debit sweeps, hourly ledger drift reapers, score recalibrations.
asynq-monitorbackend/cmd/asynq-monitorExposes Prometheus metrics and HTTP queue statistics for monitoring.
asynq-ui / asynq-ui-webTaskfile commandsTerminal CLI or password-protected web dashboard for inspecting queue health and dead letters.

Queue Priority Taxonomy & Concurrency Weights

Asynq uses weighted-random scheduling without strict priority starvation. Concurrency slots are distributed across 15 dedicated queues configured in backend/cmd/asynq-worker/main.go and backend/internal/jobs/constants.go:

Priority Tier / QueueDefault WeightEnvironment OverridePurpose
score5ASYNQ_SCORE_PRIORITYHigh-frequency credit score recalculations and tier transitions.
superlender3ASYNQ_SUPERLENDER_PRIORITYSuper lender qualification evaluations and grace period warnings.
domain_events3ASYNQ_DOMAIN_EVENTS_PRIORITYOutbox event delivery: funding, settlement, loan activation, money notifications.
disbursement2ASYNQ_DISBURSEMENT_PRIORITYMoney-moving disbursement scans, retry loops, and expiry checks.
insurance2ASYNQ_INSURANCE_PRIORITYPolicy purchases (Curacel, MyCover) and policy reconciliation.
notifications1ASYNQ_NOTIFICATION_PRIORITYPush notifications, SMS OTP delivery, and email reminders.
kyc1ASYNQ_KYC_PRIORITYThird-party identity verification, document OCR, and selfie liveness evaluation.
collections1ASYNQ_COLLECTIONS_PRIORITYArrears tracking, restructuring tasks, and dunning progression.
ledger1ASYNQ_LEDGER_PRIORITYPeriodic ledger mirror audit scans and drift reapers.
wallet1ASYNQ_WALLET_PRIORITYDrain-only queue for wallet initialization retries.
markov1ASYNQ_MARKOV_PRIORITYBatch Markov transition matrix calculations and risk modeling.
payout_observability1ASYNQ_PAYOUT_OBSERVABILITY_PRIORITY1-minute refresh of payout triage and balance materialized views.
closure1ASYNQ_CLOSURE_PRIORITYAccount deletion finalization and data anonymization.
support1ASYNQ_SUPPORT_PRIORITYCustomer support ticket sync and CRM integrations.
messaging1ASYNQ_MESSAGING_PRIORITYStandalone messaging webhooks (WhatsApp/Telegram/SMS).

Platform liquidity bot agents run on a dedicated queue (agent) processed by the separate backend/cmd/agent-worker daemon, ensuring agent operations scale independently and cannot starve customer money movements.


Transactional Outbox Pattern

To prevent the "Dual-Write Problem" (where a database write succeeds but publishing a message to Redis fails), Roja implements the Transactional Outbox Pattern (backend/internal/events).

1. Transactional Event Insertion

When a business service updates state (e.g. MarkTicketFunded), it writes a domain event row to outbox_events (56_outbox_events.sql) within the exact same database transaction:

events.Write(ctx, tx, "ticket.funded", payload)
2. Lease & Dispatch

The background outbox dispatcher periodically leases batches of pending events (ClaimOutboxEvents), enqueues them into Redis via Asynq, and stamps published_at.

3. Event History Archive

During claim, a Common Table Expression (CTE) automatically copies the event into event_history (59_event_history.sql).

4. Consumer Deduplication

Consumers record idempotent execution in processed_events. If a network glitch re-delivers an event, the consumer detects the receipt and skips execution.

Deliberate Table Separation: Why 3 Tables?

  • outbox_events (Queue): A transient queue designed to drain to zero. Retention workers hard-delete rows where published_at < now() - 7 days to keep the pending index fast.
  • event_history (Permanent Log): An immutable, append-only log of every domain event ever produced on the platform. It is never reaped.
  • processed_events (Idempotency Receipts): Tracks consumer-side execution receipts. It has no retention and outlives outbox events.

Runtime Diagnostics & Observability

Roja embeds production-grade profiling and runtime tracing hooks into every long-running backend service.

Go Trace Flight Recorder

Every backend daemon continuously retains a bounded in-memory Go runtime execution trace (10 seconds / 10 MiB buffer). Services write their real OS process ID to /tmp/roja-traces/<component>.pid.

To capture an instant trace snapshot of a live process without restarting:

# Capture trace snapshot of the running API server
kill -USR2 "$(cat /tmp/roja-traces/api-server.pid)"

# Inspect the generated trace in your browser
go tool trace /tmp/roja-traces/api-server-*.trace

Dedicated pprof Profilers

Dedicated pprof HTTP listeners run on dedicated loopback ports:

  • API Server: http://localhost:6060
  • Asynq Worker: http://localhost:6061
  • Asynq Scheduler: http://localhost:6062
  • Agent Worker: http://localhost:6063
  • Asynq Monitor: http://localhost:6064
# Analyze CPU profile for 30 seconds
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Inspect active heap allocations
go tool pprof http://localhost:6060/debug/pprof/heap

In Kubernetes environments, Grafana Alloy continuously scrapes these pprof endpoints using internal security tokens and streams profiles into Grafana Pyroscope for real-time flame graph visualization.


Testing Strategy

Roja enforces a multi-layered testing pyramid:


/ \ Maestro E2E (mobile/e2e)
/───\ Storybook Render Tests (Vitest + composeStories)
/─────\ Postgres Integration Harness (pgharness)
/───────\ Go Unit Tests (task backend:test)
───────────

1. Go Unit Tests

Unit tests use standard Go testing idioms with mocks for external banking and KYC APIs:

task backend:test # Runs go test ./... in backend/

2. Database Integration Tests (pgharness)

Real database integration tests run against isolated PostgreSQL instances:

task backend:test-integration # Docker-backed integration test suite

The integration packages validated by CI are listed in backend/scripts/integration-packages.txt.

3. Storybook Component Tests

Renders every UI component story across apps/web and apps/staff in JSDOM:

bun --filter @roja/storybook-web test

4. Load & Capacity Benchmarks (tools/loadtest)

Roja uses k6 to validate API latency and throughput ceilings:

task loadtest # Runs k6 capacity baseline against local/staging API

task loadtest explicitly refuses to run against production hostnames (api.getroja.com) unless ALLOW_PROD=true is set.


Deployment & GitOps Architecture

Roja separates deployment into containerized backend services and edge-deployed frontends.

Nix Flake Container Image Builder

Container images are compiled deterministically by Nix using flake.nixthere are no Dockerfiles in the repository:

  • api-image: HTTP API server.
  • jobs-image: Asynq worker and scheduler binaries.
  • agents-image: Autonomous portfolio-manager bot agent.
  • migrate-image: Atlas migration runner.
  • asynqmon-image: Queue monitoring web dashboard.

Images are distroless, minimal, and shell-free, eliminating container shell vulnerabilities.

Kubernetes & Flux GitOps

The Go backend and background workers run on a Kubernetes cluster managed via Flux GitOps (getroja/service-registry):

  • CI builds container images and pushes them to GitHub Container Registry (ghcr.io/getroja/roja-*).
  • Image automation commits new image tags to the GitOps repository.
  • Flux continuously reconciles the cluster state against Kustomize overlays:
    • apps/workers: Asynq workers, schedulers, monitors.
    • apps/agents: Autonomous liquidity agents.
  • Databases: Development and PR environments run on in-cluster CloudNativePG PostgreSQL; Production runs on managed Neon with high-availability read replicas.

Cloudflare Workers (Frontends)

All web frontends run at the Cloudflare edge:

  • apps/web & apps/staff: Built with OpenNext and deployed as Cloudflare Workers for both dev (web.roja.dev, staff.roja.dev) and prod (app.getroja.com, staff.getroja.com).
  • apps/docs & Storybooks: Deployed as static asset Cloudflare Workers:
    • Docs Staging: https://docs.roja.dev
    • Docs Production: https://docs.getroja.com
    • PR Previews: https://docs-pr-<n>.roja.dev (automatically provisioned on pull requests).

Next Steps