The problem#
This was a backend assignment for Origin: given a wall with rectangular or polygonal obstacles (windows, doors), plan a path that lets a wall-finishing robot cover the free surface, and expose it all through an API. Planning can be slow, so the API should not block on it, and someone should be able to see what the system is doing while it runs.
The approach#
I treated it as a small event-driven system rather than a single endpoint. Creating a task returns 202 Accepted straight away, a separate worker process does the geometry, and a simulated robot receives the result and reports telemetry back.
- CRUD for walls, obstacles, tasks and trajectories (about 15 endpoints), validated with Pydantic and stored in PostgreSQL on Neon through SQLAlchemy and Alembic migrations.
- Three trajectories per task (a high-precision, a standard and a fast-draft sweep, using different tool radii), so a caller can compare coverage time against detail.
- Task status and trajectory reads go through Redis first, with the database as fallback and a one-hour TTL.
- Every request and worker step is logged as JSON to Logstash, Elasticsearch and Kibana, including per-request latency and per-scenario coverage figures.
- A Locust and k6 stress-testing suite lives alongside the API.
Architecture#
The FastAPI app writes a PENDING task to PostgreSQL, caches its state in Redis and publishes a message to a durable RabbitMQ queue. A cloud worker consumes one message at a time, marks the task PROCESSING, runs the planner for each scenario, saves the trajectories and marks the task COMPLETED, then publishes the standard-sweep waypoints to the robot's own queue.
A robot simulator listens on that queue, walks the waypoints and emits progress telemetry. The API, the worker and the simulator all log through the same Logstash handler, so one Kibana dashboard shows the whole flow.
Hover or tap any part to trace what it talks to. 17 parts, 20 connections. Swipe sideways to see it all.
- 1A client POSTs a task for a wall. The timing middleware starts a timer and the router checks the wall exists in PostgreSQL.
- 2The router saves the task as PENDING, caches its state in Redis and publishes a persistent message to path_planning_queue, then returns 202 Accepted.
- 3The cloud worker takes one message, marks the task PROCESSING in PostgreSQL and Redis, and loads the wall and its obstacles.
- 4The coverage planner runs three scenarios (tool radius 0.2, 0.5 and 1.0) and the worker saves each trajectory.
- 5The worker marks the task COMPLETED, refreshes the Redis entry and publishes the Standard Sweep waypoints to the robot's queue.
- 6The client polls GET /tasks/{id}, served from Redis, then reads the trajectories from GET /trajectories/{task_id}.
- 7Secondary path: the robot simulator walks the waypoints, publishes progress telemetry, and the API, worker and robot all log JSON through Logstash into Elasticsearch for Kibana.
The planner#
The planner (planning/coverage_planner.py) is built on Shapely. It subtracts the union of obstacle polygons from the wall rectangle, cuts what is left into 2m blocks, sweeps each block with vertical zigzag lines spaced one tool diameter apart, and then orders the blocks with a small genetic algorithm that minimises the transit distance between the end of one block and the start of the next.
safe_block = block.intersection(self.free_space)
# If the remaining safe block is larger than the tool size, keep it
if not safe_block.is_empty and safe_block.area > (self.tool_radius**2):
blocks.append(safe_block)What I learned#
Putting a queue between the API and the slow work changed the design more than any algorithm did: statuses, retries, rejecting poison messages and observability all had to be thought through. I also learned to be careful about which claims a README makes versus what the code actually measures.