Architecture
Big picture
Armada is split into a control plane and a set of executors. The control plane accepts, schedules, and tracks jobs. One executor runs per worker cluster and bridges the control plane to that cluster's Kubernetes API (docs/system_overview.md:21). Clients submit jobs to the control plane, and jobs are transferred to a worker cluster only once scheduled (docs/system_overview.md:20).
The services communicate by event sourcing. Message routing goes through a log-based message broker, Apache Pulsar, shared by all services. The log is the source of truth, and each subsystem can rebuild its internal state by replaying messages (docs/system_overview.md:62-70).
Components
Control plane subsystems
The control plane is the set of components responsible for job submission and scheduling (docs/system_overview.md:54). It is made up of: job submission and control (submit, re-prioritise, cancel), job state querying, streaming job state querying, the scheduler that assigns jobs to clusters and nodes, and Lookout, the web UI showing job state (docs/system_overview.md:56-60). Each subsystem is a binary under cmd/; for example cmd/server/main.go and cmd/scheduler.
Executor
There is one executor per worker cluster (docs/system_overview.md:21). It is responsible for communication between the Armada control plane and the Kubernetes control plane (docs/system_overview.md:21). When the scheduler marks a job as leased, the executor creates the Kubernetes resources for the job in its cluster and binds the pod to the assigned node (docs/system_overview.md:45-46).
Dependency middleware
Armada relies on Apache Pulsar for message routing (docs/system_overview.md:62). The scheduler and Lookout persist to PostgreSQL, and Redis is part of the local development stack (README:81). The local stack brings up redis, postgres, and pulsar via Docker Compose (README:81).
How a request flows
A job submission walks through the submit server in internal/server/submit/submit.go. The Server type is the service that accepts the original Armada submit API and publishes messages to Pulsar based on those calls (internal/server/submit/submit.go:30-32).
- Entry point
func (s *Server) SubmitJobs(...)(internal/server/submit/submit.go:72). - Authorization: the caller is checked with
s.authorize(ctx, req.Queue, permissions.SubmitAnyJobs, queue.PermissionVerbSubmit); failure returnscodes.PermissionDenied(internal/server/submit/submit.go:76-79). - Validation:
validation.ValidateSubmitRequest(req, s.submissionConfig); failure returnscodes.InvalidArgument(internal/server/submit/submit.go:82-84). - Deduplication:
s.deduplicator.GetOriginalJobIds(ctx, req.Queue, req.JobRequestItems)maps a client id to an existing job id. Deduplication is best-effort, so an error here is logged and not fatal (internal/server/submit/submit.go:88-92). A request item that is a duplicate returns the previously submitted id and is skipped (internal/server/submit/submit.go:101-106). - Conversion: each non-duplicate item becomes a SubmitJob event via
conversion.SubmitJobFromApiRequest(...)(internal/server/submit/submit.go:109), wrapped as anEventSequence_Event_SubmitJob(internal/server/submit/submit.go:111-116). - Publish: the events are assembled into an
EventSequence(internal/server/submit/submit.go:133-139) and sent withs.publisher.PublishMessages(ctx, es); failure returnscodes.Internal(internal/server/submit/submit.go:141-145). - After a successful publish, deduplication ids are stored with
s.deduplicator.StoreOriginalJobIds(...)(internal/server/submit/submit.go:149). A comment notes that a partial Pulsar submission can result in duplicate jobs because this store only runs on success (internal/server/submit/submit.go:147-148).
The scheduler then takes over. func (s *Scheduler) Run(...) (internal/scheduler/scheduler.go:147) runs a ticker every cycle period (internal/scheduler/scheduler.go:159, :164-169). Only the leader may schedule jobs (internal/scheduler/scheduler.go:49-50) and only the leader publishes (internal/scheduler/scheduler.go:56). On becoming leader, it first catches up to all Pulsar messages with s.ensureDbUpToDate(...) before scheduling (internal/scheduler/scheduler.go:186-189). The body of one cycle is func (s *Scheduler) cycle(...) (internal/scheduler/scheduler.go:281).
Key design decisions
Event sourcing with Pulsar as source of truth is the central decision: subsystems update state and publish back to the log, and any subsystem can recover by replaying (docs/system_overview.md:62-70). The submit path therefore never writes jobs to a database; it only publishes to Pulsar, leaving persistence to downstream consumers (internal/server/submit/submit.go:141).
Scheduling is leader-only. A single leader schedules and publishes, which avoids two schedulers making conflicting placement decisions (internal/scheduler/scheduler.go:49-50, :56). To stay correct, a new leader blocks on catching up to Pulsar before it schedules (internal/scheduler/scheduler.go:186-189).
The scheduler decouples its hot loop from database I/O by holding all jobs in an in-memory transactional store, JobDb (internal/scheduler/jobdb/jobdb.go:68). This is covered in Internals.
Extension points
Armada exposes gRPC and REST submit APIs, so clients in several languages drive it without changing the server. Client libraries exist for Python, Java, and .NET (docs/client_libraries.md). An Airflow operator integrates Armada into Airflow DAGs (docs/armada_airflow_operator.md). Job scheduling behaviour is configurable through priority classes and Dominant Resource Fairness (DRF, fair allocation across multiple resource types) settings consumed by the scheduler (internal/scheduler/scheduling/fairness/fairness.go:43).