DEV Community

Leo Han
Leo Han

Posted on

Firefly: A Lightweight Distributed Scheduler for Java Applications

Firefly: A Lightweight Scheduler Center for Java Systems

Summary: Many business systems eventually outgrow scattered in-process cron jobs. They need centralized job management, remote executors, observable dispatch, and time-zone correctness. Firefly approaches this with a Java 21 scheduling service whose core, persistence, transport, API, and UI boundaries stay deliberately clear.

Tags: Java, Spring Boot, Scheduler, Distributed Systems, Firefly

Project links: Website (GitHub Pages) · GitHub Repository · Quick Start · Integration Guide · Admin API · Report an Issue

Why This Matters

Scheduled jobs often start small: one cron expression, one method, one service. As the system grows, the operational questions become sharper:

  • Which service instance is online and able to receive work?
  • How can a scheduler trigger business code without requiring every service to expose a listener port?
  • What happens when cron jobs cross time zones or daylight-saving transitions?
  • Was a failed run never dispatched, never ACKed, timed out, rejected, or exhausted after retries?
  • In a scheduler cluster, who owns a shard and how do we stop an old owner from advancing jobs?

Firefly does not try to become a large workflow platform. Its goal is narrower: provide a lightweight scheduler center for Java systems, with scheduling semantics kept in a restrained core and operational capabilities placed in independent modules.

Core Idea: Clear Boundaries

Boundary Module Responsibility
Scheduler core libs/scheduler-core cron, fixed-rate, job-level ZoneId, misfire, concurrency policy
Remote execution transports/netty, clients/executor-netty Executor-initiated connection, heartbeat, trigger, ACK, result reporting
Spring Boot integration integrations/firefly-spring-boot-starter Executor auto-configuration, @FireflyJob discovery, startup job sync
Persistence and HA stores/jdbc job, execution, outbox, node, shard lease, fencing token
Admin API apis/admin-http JSON API, authentication, RBAC, audit, job management
Operations ui/admin, plugins/metrics-prometheus Admin console and Prometheus metrics

Caption: Firefly separates business services, Gateway, scheduler center, persistence, Admin API, and Admin UI into independent boundaries.

A Practical Example: From Scattered Cron Jobs to Governed Scheduling

Imagine a billing system with three kinds of jobs:

  • Generate bills every day at 02:00.
  • Check unfinished orders every hour.
  • Let operators manually trigger compensation when needed.

If these are only scattered @Scheduled methods, the early experience is convenient, but the long-term operational model becomes weak: job definitions are not centralized, execution history is fragmented, instance liveness is unclear, and manual triggers are hard to audit.

Firefly keeps business code inside the business service, but moves job definition, triggering, execution state, audit, and metrics into a scheduler center. The difference is not "move business logic away"; it is "make the job lifecycle governable."

Fastest Integration: Spring Boot and @FireflyJob

Maven:

<dependency>
    <groupId>com.firefly</groupId>
    <artifactId>firefly-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Gradle:

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation "com.firefly:firefly-spring-boot-starter:1.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Minimal configuration:

spring:
  application:
    name: firefly-example
firefly:
  executor:
    name: billing-executor
    gateway-addresses:
      - 127.0.0.1:9700
    integration-key: ${FIREFLY_INTEGRATION_KEY}
server:
  port: 80
Enter fullscreen mode Exit fullscreen mode

Then annotate a business method:

import com.firefly.domain.ExecutionContext;
import com.firefly.spring.annotation.FireflyJob;
import org.springframework.stereotype.Component;

@Component
public class BillingJobs {
    @FireflyJob(
            name = "Daily billing",
            cron = "0 0 2 * * *",
            zoneId = "Asia/Shanghai",
            groupId = "billing",
            parameters = {"tenant=primary"}
    )
    public void billingHandler(ExecutionContext context) {
        System.out.println("executionId=" + context.executionId());
        // run business code
    }
}
Enter fullscreen mode Exit fullscreen mode

By default, the Starter uses the fully qualified method name as both the automatic entrypoint and job ID:

com.example.BillingJobs#billingHandler
Enter fullscreen mode Exit fullscreen mode

No global jobId or handlerName is required. After the Spring application is ready, the Starter registers the local handler and synchronizes the @FireflyJob declaration to Firefly Admin API. The scheduler center later triggers the handler through Gateway.

When one method needs multiple schedules, repeat @FireflyJob and provide a unique key:

@FireflyJob(key = "daily", name = "Daily billing", cron = "0 0 2 * * *", zoneId = "Asia/Shanghai")
@FireflyJob(key = "hourly", name = "Hourly billing check", cron = "0 0 * * * *", zoneId = "Asia/Shanghai")
public void billingHandler(ExecutionContext context) {
    // run business code
}
Enter fullscreen mode Exit fullscreen mode

What Happens When the Annotation Creates a Job

The value of @FireflyJob is not just fewer configuration lines. It creates a startup loop between the business method, remote executor, and scheduler center.

Caption: The Spring Boot Starter scans @FireflyJob, registers local handlers, and synchronizes job definitions through Admin API.

The sequence is:

  1. Spring Boot starts the business service.
  2. The Starter scans @FireflyJob methods and creates FireflyJobRegistration objects.
  3. Netty Executor Client actively connects to Firefly Gateway.
  4. The Starter calls Admin API to check whether the job exists.
  5. Missing jobs are created automatically; existing jobs are left unchanged by default.

This mode works well for teams that want jobs declared in code while still keeping operational governance in the Admin UI.

Why Job-Level Time Zones Matter

Firefly requires cron jobs to declare an IANA ZoneId, while runtime cursors use UTC Instant. This avoids scheduling behavior being affected by the default time zone of the machine running the scheduler.

JobDefinition job = JobDefinition.builder()
        .id("new-york-daily-report")
        .name("New York Daily Report")
        .handlerName("reportHandler")
        .schedule(new CronSchedule("0 0 9 * * *"))
        .zoneId(ZoneId.of("America/New_York"))
        .build();
Enter fullscreen mode Exit fullscreen mode

Cron is calculated in the job's local time, while persisted runtime state is stored as UTC.

Cluster, Outbox, and Observability

When Firefly runs as a standalone scheduler center, JDBC storage can persist job definitions, runtime cursors, executions, outbox records, node state, and audit logs.

Key design points:

  • Scheduler nodes acquire shard ownership through shard leases.
  • Lease renewal and takeover use fencing tokens.
  • Runtime cursor CAS, execution creation, and outbox insertion happen in one transaction.
  • Remote dispatch retries through outbox records, and ACK timeout can trigger redelivery.
  • The Prometheus plugin exposes scheduling delay, execution duration, executor connections, and database clock offset.

This moves the problem from "run a method when the clock ticks" to "make scheduling explainable across failures, restarts, network stalls, and cluster ownership changes."

Caption: Firefly calculates business time with job-level ZoneId, persists runtime cursors as UTC Instant, and dispatches remote executions through outbox records.

Think of it as two lines:

  • Time line: cron is calculated in the job's own ZoneId, while runtime state is persisted as UTC Instant.
  • Dispatch line: when a job is due, the scheduler advances the cursor, creates an execution, writes outbox, and Gateway dispatches to an online Executor.

This reduces two common failure classes: "the time was calculated wrong" and "state advanced but dispatch did not happen." Firefly models both explicitly so there is something concrete to inspect.

How It Differs From Familiar Choices

Option Best for Firefly difference
@Scheduled A few jobs inside one application Firefly adds centralized management, remote executors, execution history, and cluster governance
Quartz Complex in-process scheduling Firefly emphasizes scheduler center, Admin API, Netty Executor, and JDBC HA
XXL-JOB / PowerJob Mature task platform capabilities Firefly is lighter and keeps core boundaries more restrained
Airflow / DolphinScheduler DAGs, data orchestration, backfills Firefly does not require service jobs to become DAG tasks

Where Firefly Fits

Firefly is a good fit when:

  • Your Java or Spring Boot services need annotation-based jobs plus centralized governance.
  • Business services should actively connect to the scheduler center instead of exposing listener ports.
  • Jobs cross time zones and should not depend on machine defaults.
  • You need Admin UI, Admin API, Prometheus metrics, and audit records.
  • You want a path from local H2 to PostgreSQL/MySQL and then to multi-node clusters.

It is not meant to replace large workflow engines. If the core requirement is complex DAGs, data lineage, backfills, approval flows, or data-platform orchestration, tools such as Airflow or DolphinScheduler may be a better fit.

Try It Locally

Start Firefly Server:

.\gradlew.bat :server:launcher:run --args="--firefly.config.profile=h2"
Enter fullscreen mode Exit fullscreen mode

Default endpoints:

Service Address
Admin UI http://127.0.0.1:9720
Admin API http://127.0.0.1:9710
Metrics http://127.0.0.1:9711/metrics
Gateway 127.0.0.1:9700

If the starter artifacts are not published to a remote repository yet, publish them locally first:

.\gradlew.bat publishToMavenLocal
Enter fullscreen mode Exit fullscreen mode

Three Sentences for Introducing Firefly

If you need to explain Firefly quickly, these three sentences work:

  1. Business code stays in the business service; job governance moves to the scheduler center.
  2. Executors actively connect to Gateway, so business services do not need to expose listener ports just to be scheduled.
  3. Time semantics, runtime cursors, outbox, and shard leases are explicit, making scheduling issues easier to inspect.

Production Integration Checklist

  • Jobs explicitly declare IANA ZoneId.
  • Business handlers are idempotent by executionId or business key.
  • Integration Key is injected through environment variables or a secret manager.
  • Admin API, Gateway, and Metrics ports are separated per environment.
  • PostgreSQL/MySQL schema initialization strategy is explicit.
  • Prometheus alerts cover schedule delay, outbox stalls, executor connections, and database clock drift.

Conclusion

Firefly's value is not in packing every scheduling-platform feature into one service. Its value is in making the difficult boundaries explicit: time semantics, executor connectivity, persistent runtime state, cluster ownership, management APIs, and operational visibility.

For Java teams moving from "a few scheduled methods inside services" toward "a governed scheduler center", Firefly offers a lightweight and understandable path.

Top comments (0)