Skip to content
Owner's local time in SP:
docs / projects / orbit

Orbit

A job scheduler whose foundations exist and whose scheduler does not, yet.

early developmentjava · spring boot · postgresqlsource ↗

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.

Important

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.

CapabilityStateNotes
Create, list, read, update, enable and disable jobsimplementedREST API in JobController; there is no delete endpoint
Record an execution of a job on demandimplementedcreates a QUEUED record; nothing picks it up
List and read executionsimplementednewest first per job
Execution state machineimplemented in the domain onlystart, succeed, fail and cancel are tested, but no service or endpoint calls them
Schema and migrationsimplementedFlyway V1 and V2 on PostgreSQL
Cron schedulingplannedthe expression is stored and checked for blankness, not interpreted
Workers, claiming and heartbeatsplannedno worker module exists
Timeoutsplannedtimeout_seconds is stored, not enforced
Retries and backoffplannedmax_retries is stored; the retry semantics are still a design issue
Cancellation of running workplannedthe state transition exists, propagation to a worker does not
Logs and live updatesplannedno log storage or streaming
Authenticationplannedthe API accepts unauthenticated requests
Web interface and packagingplannedno 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 · web

The 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.

sql
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--> CANCELLED

SUCCEEDED, 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 Job and Execution invariants 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 command field 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.properties for 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.

References