Kangaroo Paw is a backend scheduling system for kitchen execution. It explodes orders into atomic tasks, applies dependency-aware readiness, assigns tasks under staff/counter/machine constraints, and drives execution using Kafka + Temporal.
- REST API with router/controller/service/repository layering
- PostgreSQL as system-of-record (orders, tasks, dependencies, counters, machines, staff, domain events)
- Redis sorted set ready queue (
tasks:pending) for fast priority lookup - Kafka topic for domain-event transport (
kitchen.domain.events) - Temporal workflows for asynchronous allocation and auto-completion timing
- Outbox dispatcher for reliable event publishing with retry/backoff
- Assignment timeout requeue for stale
ASSIGNEDtasks - Recipe and dependency model fully DB-driven (no hardcoded recipe map)
POST /api/v1/orders/confirmrecipe_stepsare fetched from Postgres and one task per step is created.- Task dependencies are built from:
- explicit
recipe_step_dependencies(preferred), or - fallback inference (assembly fan-in + single-capacity machine serialization).
- explicit
- Tasks with
pending_deps = 0are pushed to Redis sorted set and correspondingTASK_READYis inserted into outbox (domain_events). - Outbox dispatcher publishes events to Kafka with retries and marks events as published.
- Topic consumer reads Kafka and starts Temporal workflows.
TASK_READY,TASK_COMPLETED,TASK_REQUEUED, etc. -> Allocation workflowTASK_STARTED-> Auto-complete workflow (sleepestimate_secs, then complete)
- Allocation transaction (with locks) selects feasible task/staff/machine and assigns.
- Manual start API marks task
STARTED. - Temporal auto-complete marks task
COMPLETED, releases capacities, updates staff metrics, unlocks child tasks, emitsTASK_READY.
CONFIRMEDIN_PROGRESS(when first task is assigned)PART_COMPLETED(when at least one task is completed)COMPLETED(when all tasks are completed)
UNASSIGNEDASSIGNEDSTARTEDCOMPLETED
- A task is assignable only when:
pending_deps = 0- counter has available capacity
- machine is up and has available capacity
- eligible staff exists (skill match, in shift, not on break, below max parallel)
- Staff pick score:
weightLoad*active_tasks + weightUtil*utilization - weightEff*efficiency_multiplier
- Aging priority score in ready queue:
basePriority + agingFactor * waitTime(stored as negative for min-pop)
- Stale assignment protection:
- tasks left
ASSIGNEDwithoutSTARTEDbeyond TTL are atomically requeued with boosted priority andTASK_REQUEUEDevent.
- tasks left
cmd/
api/
topic-consumer/
temporal-worker/
kafka-tail/
internal/
api/
router/
controllers/
application/
repository/
models/
messaging/
temporalx/
orchestrator/
queue/
outbox/
db/
config/
db/
schema.sql
migrations/
docker/
postgres/init/
- API:
http://localhost:8080 - PostgreSQL:
localhost:5432 - Redis:
localhost:6379 - Kafka broker (Redpanda):
localhost:9092 - Kafka Console:
http://localhost:8081 - Temporal frontend:
localhost:7233 - Temporal UI:
http://localhost:8088
- Host:
localhost - Port:
5432 - DB:
kangaroo_paw - User:
postgres - Password:
postgres
JDBC:
jdbc:postgresql://localhost:5432/kangaroo_paw
docker compose up --build -dCheck status:
docker compose ps
curl -s http://localhost:8080/healthExpected health response:
{"status":"ok"}If you already had data volume before recent refactors, apply these once:
docker exec -i kangaroo-postgres psql -U postgres -d kangaroo_paw < db/migrations/20260302_counter_fk_normalization.sql
docker exec -i kangaroo-postgres psql -U postgres -d kangaroo_paw < db/migrations/20260302_machine_fk_normalization.sql
docker exec -i kangaroo-postgres psql -U postgres -d kangaroo_paw < db/migrations/20260302_outbox_reliability.sql
docker exec -i kangaroo-postgres psql -U postgres -d kangaroo_paw < db/migrations/20260302_recipe_step_dependencies.sql
docker exec -i kangaroo-postgres psql -U postgres -d kangaroo_paw < db/migrations/20260302_seed_recipe_step_dependencies.sqlGET /healthPOST /api/v1/orders/confirmPOST /api/v1/allocator/run-oncePOST /api/v1/tasks/{taskID}/startPOST /api/v1/tasks/{taskID}/completeGET /api/v1/tasks/{taskID}
Example order confirm:
curl -X POST http://localhost:8080/api/v1/orders/confirm \
-H 'content-type: application/json' \
-d '{"external_order_id":"ord-1001","items":{"burger_combo":1,"cold_coffee":2,"wrap":1}}'Manual start (completion is automated by Temporal timer):
curl -X POST http://localhost:8080/api/v1/tasks/5/startKafka messages:
- Web UI:
http://localhost:8081-> Topics ->kitchen.domain.events - CLI:
docker exec -it kangaroo-kafka rpk topic consume kitchen.domain.events -f '%o %k %v\n'Redis queue:
docker exec -it kangaroo-redis redis-cli ZCARD tasks:pending
docker exec -it kangaroo-redis redis-cli ZRANGE tasks:pending 0 50 WITHSCORESPostgres quick checks:
docker exec -it kangaroo-postgres psql -U postgres -d kangaroo_paw -c '\dt'
docker exec -it kangaroo-postgres psql -U postgres -d kangaroo_paw -c 'SELECT id,status,created_at FROM orders ORDER BY id DESC LIMIT 10;'
docker exec -it kangaroo-postgres psql -U postgres -d kangaroo_paw -c 'SELECT id,order_id,status,pending_deps,assigned_staff_id,assigned_machine_id,assigned_at,started_at,completed_at FROM tasks ORDER BY id DESC LIMIT 30;'
docker exec -it kangaroo-postgres psql -U postgres -d kangaroo_paw -c 'SELECT id,event_type,attempts,published_at,next_retry_at,last_error FROM domain_events ORDER BY id DESC LIMIT 30;'go test ./...Added test:
internal/integration/bulk_orders_test.go
What it validates:
- concurrent creation of multiple
burger_comboorders - task creation and dependency gating
- repeated manual task starts via API
- auto-completion via Temporal timer
- final
COMPLETEDorder/task states - assemble-step starts only after all prep steps complete
Run it against running local stack:
INTEGRATION_TEST=1 go test ./internal/integration -run TestBulkOrdersEndToEnd -vOptional overrides:
INTEGRATION_API_BASE(defaulthttp://localhost:8080)INTEGRATION_PG_DSN(default local postgres dsn)
- Recipe behavior is data-driven from DB; add/edit steps using
recipe_stepsand dependencies usingrecipe_step_dependencies. - For best observability, keep Kafka Console and Temporal UI open while running load/integration tests.