A distributed job scheduling and execution engine — submit background jobs over REST, and TaskForge handles queuing, scheduled/delayed execution, automatic retries with exponential backoff, dead-lettering, and idempotent resubmission.
Think of it as a small, self-hosted version of what sits behind "send this email in 10 minutes," "retry this payment webhook 5 times," or "process this file asynchronously" at companies like DoorDash, Stripe, or Uber.
Most "todo API" portfolio projects don't demonstrate distributed-systems thinking. TaskForge is built around three problems every backend engineer runs into at scale:
| Problem | How TaskForge solves it |
|---|---|
| A client retries a request after a timeout — do you process the job twice? | Idempotency keys: resubmitting the same key returns the existing job instead of creating a duplicate |
| A job fails — do you lose it, or hammer the downstream service instantly? | Exponential backoff retries (2s → 4s → 8s → 16s...) via a SCHEDULED state, capped by maxAttempts |
| A job keeps failing — does it retry forever? | Dead-letter queue: after max attempts, the job is marked DEAD_LETTERED and published to a DLQ topic for inspection |
| Two worker instances read the same message during a Kafka rebalance | Distributed lock via Redis (SET NX PX) so only one worker executes a given job |
Client --REST--> [Job API] --save--> [Postgres]
|
v
[Kafka: taskforge.jobs] --> [Worker(s)] --Redis lock--> execute()
^ |
| on failure
[Scheduler: promotes due (retry or)
SCHEDULED jobs every 5s] v
[Kafka: taskforge.jobs.dlq]
- API layer (Spring Boot REST) — accepts job submissions, persists to Postgres, publishes job IDs to Kafka.
- Queue (Kafka) — decouples submission from execution; keyed by job ID so retries preserve per-job ordering.
- Workers (Kafka consumer group) — pull jobs, acquire a short-lived Redis lock, execute, and update status.
- Scheduler — a lightweight
@Scheduledpoller that promotesSCHEDULEDjobs (both delayed jobs and retry backoffs) back onto the queue when they're due.
Java 17 · Spring Boot 3 · Spring Kafka · Spring Data JPA · PostgreSQL · Redis · Docker Compose
Requires Docker and Docker Compose.
git clone <your-repo-url>
cd taskforge
docker-compose up --buildThis starts Postgres, Redis, Zookeeper, Kafka, and the TaskForge app on localhost:8080.
Submit a job
curl -X POST http://localhost:8080/api/jobs \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "order-42-confirmation-email",
"type": "flaky-send-email",
"payload": "{\"to\":\"user@example.com\"}",
"priority": 5,
"maxAttempts": 5
}'Check job status
curl http://localhost:8080/api/jobs/{id}List jobs by status
curl "http://localhost:8080/api/jobs?status=DEAD_LETTERED"Job types prefixed with flaky- intentionally fail ~40% of the time (see JobConsumer.execute) so you can watch the retry → backoff → DLQ flow without wiring up a real downstream integration. Swap this method out for actual work (send an email, call a webhook, generate a report) when adapting this for a real use case.
TaskForge needs Postgres, Redis, and Kafka, so a pure serverless deploy isn't the easiest path — the cleanest free/cheap options:
- Render — deploy the
taskforgeservice as a Docker Web Service. Add a Render Postgres instance (free tier) and a Render Redis/Key-Value instance. - Kafka: self-hosting Kafka on a free tier is painful. Easiest path is Confluent Cloud (free trial credits) — just point
KAFKA_BOOTSTRAP_SERVERS(and add SASL config toapplication.yml) at your Confluent cluster instead of the local broker. - Alternatively, for a resume demo, run the whole
docker-compose.ymlstack on a small VM (e.g. an AWS EC2 free-tier instance or an Oracle Cloud free VM) — simplest to set up end-to-end since it mirrors local dev exactly.
Set these environment variables wherever you deploy: DB_URL, DB_USER, DB_PASSWORD, REDIS_HOST, REDIS_PORT, KAFKA_BOOTSTRAP_SERVERS.
MIT — free to use as a reference or starting point.