DEV Community

Cover image for Scheduling Is a Domain Problem, Not a Framework Problem
Leon Pennings
Leon Pennings

Posted on • Originally published at blog.leonpennings.com

Scheduling Is a Domain Problem, Not a Framework Problem

Scheduling in software often looks like a library search: “I need to run a job at 2 AM.” “Okay, add Quartz.” “Add Spring Scheduler.”

We have been trained to treat scheduling as a mechanism problem. Developers instinctively reach for cron expressions, triggers, and annotations because they provide a ready-made technical solution. But when you outsource your schedule to a framework, you often accidentally outsource your business rules, too.

The real problem, at its core, is a domain problem, not an execution problem.

In this article, I’ll show why scheduling is first and foremost about domain intent, how a small, domain-first scheduler (which I call Deadpool) can be implemented in a few hundred lines, and why clean code emerges naturally when the domain model is correct.


1. Policy vs. Mechanism: The Core Principle

At the heart of scheduling, there are two separate concerns that most frameworks conflate:

  1. Policy (Domain Decision) This defines what should happen, when, how often, and under what constraints.
* *Which jobs exist?*

* *What are the retry rules?*

* *Can this job run if the user is suspended?*

* *Is this a batch job or a transactional job?*
Enter fullscreen mode Exit fullscreen mode
  1. Mechanism (Execution) This defines how the computer actually runs the task.
* *Thread pools and context switching.*

* *Timing loops.*

* *Shutdown hooks.*

* *Concurrency locks.*
Enter fullscreen mode Exit fullscreen mode

The Key Insight: If policy and mechanism are mixed, domain logic gets buried under infrastructure concerns. Frameworks often force the domain to adapt to their constraints (e.g., "You must fit your logic into a cron expression") instead of the infrastructure serving the domain.


2. Frameworks Alone Are Not Enough

Frameworks like Quartz or Spring Scheduler provide powerful features: persistence, clustering, failover, and complex triggers.

But blindly applying a framework often leads to a domain-model inversion: the framework drives the design, and the domain has to fit its API.

The decision of which work to execute and when is a domain problem.

  • Domain rules determine which jobs should be submitted.

  • The Scheduler executes them but should not interpret the rules.

  • The Domain governs batching, concurrency, and frequency.

Even at high scale, a domain-first approach ensures that intent remains clear, while the scheduler remains a dumb, reliable execution engine.


3. Deadpool: A Domain-First Scheduler

To demonstrate these principles, I implemented a minimal domain-driven scheduler I call Deadpool (as it executes jobs). It is framework-agnostic and captures the domain-first philosophy.

3.1 Design Goals

Deadpool enforces several invariants while keeping the implementation simple:

  1. Threaded execution: Jobs run concurrently in a controlled thread pool.

  2. Domain-defined concurrency: The Job decides if it allows overlap, not the scheduler.

  3. Self-monitoring: Jobs define their own timeouts; the scheduler just enforces them.

  4. Simple abstraction: DeadpoolJob encapsulates the logic.

3.2 Implementation Highlights

The core is just over a hundred lines of code. Notice how the Scheduler asks the Job for instructions (getDelayInMinutes, isConcurrent), effectively asking Policy from the Domain.

public abstract class DeadpoolJob implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(DeadpoolJob.class);

    private String type;
    private LocalDateTime runDate;

    // ... constructors ...

    boolean runNow() {
        return runDate == null;
    }

    @Override
    public void run() {
        started = LocalDateTime.now();
        // POLICY CHECK: The domain decides if it is allowed to run
        if (Deadpool.mayRun(this)) { 
            LOGGER.info("Starting job " + type);
            final DeadpoolJob masterJob = this;

            // WATCHDOG: The job defines its own timeout policy
            DeadpoolJob cancelJob = new DeadpoolJob("Cleanjob for " + getJobIdentification(), 
                                                  LocalDateTime.now().plusHours(getJobTimeoutInHours())) {
                @Override
                public void run() {
                    //check if main job is still running and have Deadpool terminate the job
                    Deadpool.checkRuntime(masterJob);
                }
                // ... implementation omitted
            };

            Deadpool.execute(cancelJob);
            try {
                performHit(); // MECHANISM: The actual work happens here
                completedTime = LocalDateTime.now();
            } catch (Throwable t) {
                handleFailure(t);
            }
            //indicate we are complete and terminate the cancel job
            Deadpool.reportComplete(this, cancelJob);
        }
    }
    //implemented in subclass
    public abstract void performHit();

    // POLICY: Domain decides how to handle failures
    protected void handleFailure(Throwable t) {
        // Default: Log and stop, no retry
        LOGGER.error("Job failed: " + type, t);
        // Subclasses can override for retries, alerts, etc.
    }

    public boolean isConcurrent() {
        return false; // Default Policy
    }

    // POLICY: The job defines the schedule, not an external cron string
    public long getRepeatDelay() { return 24 * 60; }
    public TimeUnit getRepeatTimeUnit() { return TimeUnit.MINUTES; }

    public long getDelayInMinutes() {
        return Math.max(0, Duration.between(LocalDateTime.now(), runDate).toMinutes());
    }
}
Enter fullscreen mode Exit fullscreen mode

The scheduler (Mechanism) is purely reactive. It manages the threads, but follows the orders of the DeadpoolJob.

public class Deadpool {
    // Standard ThreadPool setup...

    public static void execute(DeadpoolJob deadpoolJob) {
        Future future;
        // MECHANISM: Simply putting the task into the pool based on Domain instructions
        if (deadpoolJob.runNow()) {
            future = executor.submit(deadpoolJob);
        } else {
            // ... scheduling logic ...
        }
        FUTURES_MAP.put(deadpoolJob, future);
    }

    // ... shutdown and cleanup logic ...

    static synchronized boolean mayRun(DeadpoolJob deadpoolJob) {
        // CONCURRENCY POLICY: Enforced here, defined by the job
        if (deadpoolJob.isConcurrent()) {
            return true;
        }
        if (!TYPE_MAP.contains(deadpoolJob.getType())) {
            TYPE_MAP.add(deadpoolJob.getType());
            return true;
        }
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

3.3 What the Developer Sees: A Concrete Example

The beauty of this design is the clarity for the developer. To create a daily task, you simply extend the class. The class defines its own existence in basic java.

public class CurrencyConverterUpdater extends DeadpoolJob {

    public CurrencyConverterUpdater() {
        // POLICY: Name and Initial Start Time
        super("CurrencyConverter", LocalDateTime.now().withHour(7).withMinute(0));
    }

    @Override
    public void performHit() {
        // MECHANISM: Domain logic. No annotations or CRON strings required.
        CurrencyConverter.updateRates();
    }

    // Optional: Override getRepeatDelay() if this isn't a 24h job
}
Enter fullscreen mode Exit fullscreen mode

4. Scaling Considerations: Statelessness via State

Scaling is often cited as the reason to use heavy frameworks. But scaling is also a domain problem.

If you run multiple Deadpool instances, they can remain completely stateless. The coordination happens where the truth lives: in the database.

The Pattern:

  1. Work items reside in a central database (e.g., NotificationRequest).

  2. Each job acts as a Command: it queries for unclaimed records.

  3. It claims them transactionally (Optimistic Locking/Versioning).

  4. Only after a successful commit does the job process the record.

This approach ensures Safety (no double-processing) and Scalability (add as many Deadpool instances as you want) without changing a single line of the scheduler code.


5. Modeling First Saves Code

By modeling domain intent first, boilerplate disappears:

  • No cron parsing or configuration DSLs.

  • No hidden concurrency management logic in XML/Annotation files.

  • No glue code for retries.

This validates the model. If your domain logic can be implemented this cleanly, the model is likely correct. Simplicity and flexibility are not just "nice to haves"—they are evidence of structural integrity.


6. Domain-First at Scale: Millions of Jobs

Does this hold up for massive loads? Yes.

Consider a multi-tenant SaaS sending millions of notifications. A framework-first approach would struggle with millions of triggers. A domain-first approach treats these as Data, not Config.

The "Job Intent" Pattern

  1. Intent Creation: A high-level rule (Policy) creates NotificationRequest rows. These represent the intent to do work.

  2. Batch Execution: Deadpool jobs pick up these requests in batches.

  3. Tenant Governance: The domain enforces rules like "Tenant A can only send 100 emails/minute" before the work is ever claimed.

  4. Cleanup: Completed requests are archived or deleted.

Because the Domain controls the batching and the claiming, you don't need a complex distributed scheduler. You just need a loop that asks the Domain: "What is the next most important thing to do?"


7. Conclusion

Scheduling is first a domain problem, then an execution problem.

Deadpool demonstrates that when you let Domain Intent drive the design:

  • The scheduler becomes simple and invisible.

  • The job logic becomes testable and portable.

  • The system can scale horizontally without complex infrastructure.

Clean code is not a style—it is evidence of a correct model.

When your domain model is correct, the code looks clean naturally. Correct models simplify implementation, scale beyond any framework, and provide an enormous lifespan advantage.

Top comments (0)