Orchestrator, The Manager II — API
December 24, 2025
Why the Manager Needs an API
Up until now, we’ve mostly focused on the internal mechanics of the manager. The next logical step is to expose the manager to users.
Here, “users” means application developers who want to run their workloads on our orchestration system. They shouldn’t care about workers, containers, or scheduling details. They just want a clean interface to interact with the system.
So, we build an API for the manager.
This API wraps the manager’s core functionality and exposes it in a user-friendly way.
What the Manager API Should Do
The manager API needs to support only a small set of operations:
- Send a task to the manager
- Get a list of all tasks in the system
- Stop a task
Just like the worker API, the manager API is built using:
- Handlers
- Routes
- A router (mux)
Zooming Out: The Big Picture
Before diving into code-level details, it helps to zoom out and look at the overall architecture.
The manager and worker are abstractions that free developers from having to think about the underlying infrastructure—whether it’s physical machines or virtual ones.
Each of them follows the same basic pattern:
- An API server
- A core component (manager logic or worker logic)

This symmetry is intentional and makes the system easier to reason about.
Manager API Routes
Now let’s zoom back in and look at the manager’s API itself.
The routes are intentionally very similar to the worker’s API:
-
GET
/tasksReturns a list of all tasks in the system -
POST
/tasksCreates (starts) a new task -
DELETE
/tasks/{taskID}Stops a task identified bytaskID
All data sent to the API must be JSON-encoded, and all responses are returned as JSON as well.
Manager API Summary
| Method | Route | Description | Request Body | Response Body | Status |
|---|---|---|---|---|---|
| GET | /tasks |
Gets a list of all tasks | None | List of tasks | 200 |
| POST | /tasks |
Creates a task | JSON task.TaskEvent |
None | 201 |
| DELETE | /tasks/{taskID} |
Stops the task identified by taskID | None | None | 204 |
The API Struct
Just like the worker, the manager has its own Api struct.
The structure is almost identical to the worker’s API. The only real difference is this:
- The
Workerfield is replaced with aManagerfield
In other words, the API now wraps a manager instance instead of a worker instance.
Handling Requests
The manager API defines three handler methods:
StartTaskHandler(w http.ResponseWriter, r *http.Request)
GetTasksHandler(w http.ResponseWriter, r *http.Request)
StopTaskHandler(w http.ResponseWriter, r *http.Request)
These handlers translate HTTP requests into manager actions.
StartTaskHandler
This handler expects a JSON-encoded request body.
High-level flow:
- Decode the request body into a
task.TaskEvent - Check for decoding errors
- Add the task event to the manager’s queue using
AddTask - Return a
201 Createdstatus
Notice that the handler does not start tasks directly. It simply enqueues intent.
GetTasksHandler
To support this handler cleanly, we add a helper method to the manager:
GetTasks()
This method:
- Creates a slice of
*task.Task - Iterates over the manager’s
TaskDb - Appends each task to the slice
- Returns the slice
The handler then:
- Sets the response headers
- Encodes the task list as JSON
- Writes it to the response
StopTaskHandler
This handler mirrors the worker’s stop handler closely.
Steps:
- Extract
taskIDfrom the request path - Check if the task exists in the manager’s database
- Create a new
task.TaskEventwrapping the task - Make a copy of the task to avoid mutating existing state
- Add the task event to the manager’s queue
- Return a
204 No Contentresponse
Again, the handler is only expressing desired state, not performing the stop itself.
Separation of Concerns (Again)
Just like with the worker API, the manager’s handlers:
- Do not start or stop tasks directly
- Only enqueue intent
The actual execution happens elsewhere. This keeps API logic clean and focused.
Serving the Manager API
Serving the manager API looks almost identical to serving the worker API.
We reuse the same initRouter pattern:
- Create a router
- Define routes
- Attach handlers
Then we start the server using http.ListenAndServe.
Because the routes are the same as the worker’s, very little new code is required here.
Refactoring for Cleaner Code
As the system grows, a few refactorings make life easier.
Moving runTasks into the Worker
Previously, runTasks lived in main.go. This isn’t ideal.
We move it into the Worker itself so all worker behavior is encapsulated.
Changes made:
- Rename
RunTask→runTask - Rename
runTasks→RunTasks - Update
RunTasksto callrunTask
This keeps main.go cleaner and more focused on wiring components together.
Manager Loop Refactoring
We also refactor the manager slightly:
- Rename
UpdateTasks→updateTasks - Add a new public
UpdateTasksmethod that runs an infinite loop - Add a
ProcessTasksmethod that repeatedly callsSendWork
These methods mirror the worker’s looping behavior and make the manager easier to run.
Putting It All Together (main.go)
Finally, we wire everything together in main.go.
High-level flow:
- Read host and port values from the environment
- Start the worker API
- Start the worker’s
RunTasksloop - Create a list of workers (
<host>:<port>) - Create a manager instance
- Create the manager API (
mapi) -
Start manager background loops:
ProcessTasksUpdateTasks
- Start the manager API server
The manager loops run in separate goroutines, while the main goroutine blocks on the API server.
With this in place, users finally have a single entry point into the system.
They talk only to the manager. The manager coordinates workers. Workers execute tasks.
The architecture is still simple, but it’s now structured in a way that closely resembles real-world orchestrators — just without the complexity (yet).