Orbit
A job scheduler whose foundations exist and whose scheduler does not, yet.
Overview
Orbit is meant to be a self-hosted distributed job scheduler: you define jobs, Orbit runs them on a schedule or on demand, and every run is recorded on its own so there is a history of what ran, when, and with what outcome. In the intended design, workers on different machines receive the executions and report results.
Orbit does not schedule or run anything yet. Today the server stores job definitions, records executions, and enforces the rules for an execution’s life cycle. No code reads a cron expression, starts a command, enforces a timeout or retries a failure.
Current state
Verified against the source repository at commit 2967494, the latest of 21 commits. The repository has no continuous integration workflow and no release.
| Capability | State | Notes |
|---|---|---|
| Create, list, read, update, enable and disable jobs | implemented | REST API in JobController; there is no delete endpoint |
| Record an execution of a job on demand | implemented | creates a QUEUED record; nothing picks it up |
| List and read executions | implemented | newest first per job |
| Execution state machine | implemented in the domain only | start, succeed, fail and cancel are tested, but no service or endpoint calls them |
| Schema and migrations | implemented | Flyway V1 and V2 on PostgreSQL |
| Cron scheduling | planned | the expression is stored and checked for blankness, not interpreted |
| Workers, claiming and heartbeats | planned | no worker module exists |
| Timeouts | planned | timeout_seconds is stored, not enforced |
| Retries and backoff | planned | max_retries is stored; the retry semantics are still a design issue |
| Cancellation of running work | planned | the state transition exists, propagation to a worker does not |
| Logs and live updates | planned | no log storage or streaming |
| Authentication | planned | the API accepts unauthenticated requests |
| Web interface and packaging | planned | no Angular application and no application image |
The planned rows correspond to open issues in the repository, for example the scheduler loop, atomic execution claiming, worker loss, retry semantics and authentication. Several are labeled as needing a decision before implementation.
Architecture
The server is a Spring Boot 4.1.1 application on Java 25, built with Maven. It uses Spring Web MVC, Spring Data JPA, Bean Validation and Flyway, and talks to PostgreSQL. The only infrastructure file is a Docker Compose service that starts PostgreSQL 17 for development.
Code is organized by feature, then by layer:
dev.raioviajante.orbit job/ domain · application · infrastructure · web execution/ domain · application · infrastructure · webThe domain layer holds JPA entities that guard their own invariants. The application layer holds services, the infrastructure layer holds Spring Data repositories, and the web layer holds controllers and request and response records. Responses are explicit records, so entities never leave the server.
spring.jpa.hibernate.ddl-auto is set to validate: Hibernate checks the schema but never changes it, and Flyway owns every change. spring.jpa.open-in-view is off.
Data model
A job describes work. An execution is one run of a job. One job has many executions, and an execution belongs to exactly one job.
CREATE TABLE jobs ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(120) NOT NULL, command TEXT NOT NULL, cron_expression VARCHAR(100), enabled BOOLEAN NOT NULL DEFAULT TRUE, max_retries INTEGER NOT NULL DEFAULT 0, timeout_seconds INTEGER NOT NULL DEFAULT 300, -- created_at and updated_at omitted CONSTRAINT jobs_max_retries_check CHECK (max_retries >= 0), CONSTRAINT jobs_timeout_seconds_check CHECK (timeout_seconds > 0));The executions table repeats command, max_retries and timeout_seconds from the job, and adds status, created_at, started_at, finished_at, exit_code and error_message. A database check restricts status to the five known values, and an index covers job_id.
Copying the three settings is deliberate: an execution is created from a snapshot of the job, so editing a job later does not rewrite what a past execution was meant to run. The architecture notes mention an attempt number for executions, but the schema has no such column yet.
Execution life cycle
An execution starts as QUEUED. The domain object accepts only these transitions and rejects anything else with an IllegalStateException:
QUEUED --start--> RUNNINGRUNNING --succeed--> SUCCEEDED (records the exit code)RUNNING --fail--> FAILED (records exit code and a non-blank message)QUEUED | RUNNING --cancel--> CANCELLEDSUCCEEDED, FAILED and CANCELLED are terminal: no method leaves them. A job that is disabled cannot create executions, and asking for one answers with a conflict. Because nothing calls start, succeed or fail outside tests, every execution created through the API stays QUEUED.
Validation
Input is checked twice. Request records use Bean Validation: the name is required and at most 120 characters, the command is required, the cron expression is optional but cannot be blank or exceed 100 characters, retries must be zero or more, and the timeout must be greater than zero. The domain classes then enforce most of the same rules again, so an entity cannot be built in an invalid state from any caller. The cron expression is not parsed, so a value that is not valid cron is currently accepted.
Testing
The repository has 40 test methods, which were read but not run for this page:
- Unit tests for the
JobandExecutioninvariants and transitions. - Service tests with Mockito for job and execution use cases.
- Repository tests against a real PostgreSQL container started by Testcontainers.
- A Spring context load test.
There are no web-layer tests for the controllers and no end-to-end tests, so the HTTP status mapping is read from the code rather than covered by tests.
Limitations
- Nothing runs commands. The
commandfield is stored text. - There is no authentication or permission model, so the API must not be exposed beyond a trusted network.
- The default datasource credentials live in
application.propertiesfor local development. - There is no scheduling, claiming or locking, so two server instances would not coordinate anything yet.
- There is no packaging: running Orbit means building it with Maven and starting PostgreSQL from the Compose file.
Future direction
The design notes and open issues describe, in no committed order, a scheduler loop for due jobs, cron semantics, protection against duplicate scheduled executions, a Java worker with registration and heartbeats, atomic claiming, timeouts, retries, cancellation, persisted and streamed logs, authentication, an Angular web interface and container packaging. Until each lands, treat it as intent. The API reference documents only what exists.