Parallel Dataflow Engine
Build an engine in Scala that runs a set of dependent tasks (a dataflow graph, like a small version of Airflow or a build tool) as fast as possible: tasks whose inputs are ready run in parallel, up to a concurrency limit, while tasks that depend on others wait for them. When a task fails, it's retried, then everything that depends on it is cancelled or skipped, while independent branches keep going. Built with Cats Effect, the engine handles cancellation and timeouts safely, and an http4s API lets you submit graphs, watch their progress and see why a task failed. You'll learn directed acyclic graphs and topological order, structured concurrency with fibers, safe cancellation, retries, and testing concurrent programs with controlled time.
- Estimate
- ~14.5h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
Cats Effect · http4s
Project roadmap
- 01
Design the engine
~2hWrite DESIGN.md: a graph has tasks (id, the ids it depends on, the kind of work, a timeout and retry settings); results of dependencies are passed to the tasks that need them; a task's state is pending, running, succeeded, failed, skipped or cancelled; what happens downstream of a failure (skip dependents, keep independent branches, or fail fast as an option); and the global concurrency limit. Done when: DESIGN.md covers states, failure handling and concurrency.
- 02
Validate the graph
~1.5hModel graphs as case classes and validate them: unique ids, every dependency exists, and no cycles (detected with a depth-first search that reports the cycle's path). Compute a topological order for display. Done when: tests show cycles and missing dependencies are reported clearly, and valid graphs are accepted.
- 03
Run tasks in parallel
~3hWrite the scheduler with Cats Effect: keep each task's state in a Ref, start every task whose dependencies have succeeded, run them as fibers limited by a Semaphore, and when a task finishes, start the ones it unblocked. Pass dependency results to each task. Apply each task's timeout with IO.timeout. Done when: a diamond-shaped graph runs its middle tasks in parallel, and the final task gets both results.
- 04
Handle failures and cancellation
~2.5hRetry failed tasks with exponential backoff up to their limit. When a task finally fails, mark its dependents skipped while independent branches continue, or cancel all running tasks in fail-fast mode. Cancelling a whole run cancels running fibers and releases resources safely (use Resource and guarantee for cleanup). Done when: tests show skipped dependents, continuing branches, and clean cancellation.
- 05
Add the HTTP API
~2hAdd http4s endpoints: POST /runs (submit a graph as JSON, validated with clear errors), GET /runs/{id} (each task's state, attempts, duration and error), GET /runs/{id}/events as a stream of state changes (server-sent events), and DELETE /runs/{id} to cancel. Keep runs in memory with a limit on how many are kept. Done when: a graph can be submitted, watched live and cancelled over HTTP.
- 06
Add observability
~1hLog every state change with the run, task, attempt and duration, and compute per-run statistics: total time, the critical path (the longest chain of dependencies) and how much parallelism was achieved. Done when: finished runs report their critical path and statistics.
- 07
Test it
~2.5hWrite tests with munit-cats-effect and TestControl to control time: parallelism within the limit, dependency order, retries with backoff, skipping and fail-fast, timeouts, cancellation mid-run, and a property-based test with random acyclic graphs checking no task ever starts before its dependencies finish. Done when: sbt test passes reliably.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack