AI has made scaffolding infrastructure painfully easy. Add a library, copy the sample config, and start the app. Once done, declare the problem solved.
Background processing is where that habit creates the most false confidence.
A scheduler is not the same thing as a job platform, just as a timer is not the same thing as an operator surface, and a library that creates tables on startup is not the same thing as a schema strategy. Those are separate decisions, and if you blur them together, the system looks simpler than it actually is.
I recently implemented JobRunr OSS in a Spring Boot application with that exact constraint set. Existing recurring jobs were still on Spring scheduling, the application already had a real security model, and database ownership was already moving toward Flyway-managed DDL with a runtime app user restricted to DML. That meant the obvious integration path was not the right one.
This article walks through the decisions that mattered, the implementation shape that followed from them, and the small amount of code that actually carried the design.
The Real Problem Was Never "How Do We Add JobRunr?"
That is too small a question. The real questions were:
How should recurring jobs become durable?
Who owns schema changes?
What should operators see?
Who gets operational access?
Which scheduled tasks stay lightweight and which move into a job system?
Once those questions were explicit, the implementation got clearer.
The architecture I chose was:
Related Articles
Shared topics and tags
Newsletter
Expert notes in your inbox
Subscribe for new articles.
JobRunr OSS as the background job engine
Spring Boot application as the runtime host
Flyway as the owner of schema changes
A minimal application-native operator UI
root-only access to job operations
No migration of business jobs in the first implementation slice
That last point matters. I did not want infrastructure adoption and the first real workflow migration in the same change. That is how teams lose the ability to tell where risk actually came from.
How JobRunr Fits Into an Existing Spring Boot Application
The useful part of JobRunr is not the annotation story. It is the durable execution model.
The first step was simply wiring the engine into the existing app:
That was deliberate. I did not want the OSS dashboard to become an accidental part of the application boundary.
The second is more important:
jobrunr:
database:
skip-create: true
That line is the difference between "the library owns its tables" and "the platform owns its schema."
Why Flyway Had to Own the Schema
I do not like runtime DDL in production systems once the platform matures beyond a certain point.
If the target model is:
migration user performs DDL
runtime application user performs DML
then a background job library should not be allowed to create or alter schema on startup. That is a convenience feature in small systems. It becomes a liability once you care about permission boundaries and predictable deployments.
So the decision was simple:
Flyway owns the JobRunr schema. JobRunr uses the schema.
Not the other way around.
That meant generating the SQL for the exact pinned JobRunr version and committing it as a normal Flyway migration:
The exact DDL came from the pinned JobRunr version, not from guesswork and not from a loosely matched example online. That distinction matters because schema drift caused by library upgrades is exactly the kind of problem you are trying to prevent.
This was the central architectural decision of the implementation. The real question was never "can JobRunr run?" because of course it can. The real decision was:
Will runtime code be allowed to mutate schema?
My answer was no.
Why I Did Not Use the OSS Dashboard
This is the part people tend to argue about because the built-in dashboard looks like free value. Sometimes it is. More often, it is just borrowed complexity.
In this case, the application already had:
its own role model
its own navigation structure
its own session and security model
a requirement that operational access be limited to the highest-privilege internal role
That made the decision straightforward:
Do not expose the OSS dashboard as the primary operator surface
Add a small internal API
Add a minimal internal page
That is less feature-rich than a vendor dashboard. It is also cleaner.
This is the correct amount of UI for the first implementation slice.
Do not confuse "small" with "unfinished." A root-only read-only page is a valid operator surface when the job engine itself is the real first milestone.
Not Every Scheduled Task Should Move
This is where teams over-rotate.
Once a durable job engine exists, teams often want to move everything into it, and that is usually a category mistake.
Some tasks need:
Retry history
Visibility
Durable persistence
Operator awareness
Some tasks are just lightweight timers, and I do not treat those as the same problem.
So the strategy was mixed:
durable, meaningful background jobs move to JobRunr
lightweight housekeeping tasks can remain on Spring @Scheduled
That is not inconsistency. It is operational precision.
The system does not get better because one tool is used everywhere. It gets better because each behavior lives under the right model.
Why I Split Infrastructure From the First Real Job Migration
This was the other important decision. I did not migrate an existing recurring business workflow in the same ticket.
I only implemented:
the engine
the schema
the security boundary
the backend contract
the minimal UI
The first real recurring job migration was left for the next story.
That split matters because it isolates categories of risk:
infrastructure risk
schema ownership risk
security risk
workflow migration risk
If all of that lands together, teams get one large diff and one vague answer if something goes wrong. I prefer smaller truths.
What This Looks Like in Practice
After the first implementation slice, the system had:
JobRunr OSS running inside the Spring Boot app
Flyway-managed JobRunr tables
no runtime DDL by the application
root-only job status API
root-only job monitor page
existing business scheduling behavior unchanged
That last point is easy to overlook because the system was better before any business workflow was migrated.
That is the signal I look for in infrastructure work. The platform got safer and more observable before the first domain job moved onto it.
Summary
Add the job engine as infrastructure first, not as a side effect of a workflow migration
Disable runtime schema creation and keep DDL with Flyway
Use the exact pinned library version when generating schema
Prefer a small application-native operator surface when security and ownership boundaries matter
Start the UI read-only
Restrict job operations to the highest-privilege internal role
Do not migrate every scheduled task just because a new engine exists
Final Thoughts
This is the kind of engineering decision that matters more in the AI era, not less. AI can generate the dependency line, the YAML block, and half the controller. What it will not decide is whether the library should own schema, whether the built-in dashboard belongs inside your security model, or whether the first implementation slice should stop at infrastructure instead of reaching for workflow migration too early. Those are architectural decisions. And they are exactly where engineers still earn their keep.
If your team is about to add a job library by copying the quick-start, send them this first. More in the "Stop Lying to Your Stack" series.