Designing Around Eclipse's Incremental Builder: An Architecture Note for JDT-Style Projects
An architecture note on Eclipse's IncrementalProjectBuilder: why the minimal correct design is delta-driven, where the trust boundaries sit, which operational checks catch stale builds, and when to hand the build to Maven or Gradle instead.
21 May 2026, 14:40 UTC

If your Eclipse workspace recompiles everything on every save, or worse, silently serves stale class files, the problem is almost never the compiler. It is how your project participates in the incremental builder framework. The useful takeaway: Eclipse gives you per-resource change deltas for free, and the smallest correct design is one that consumes those deltas, reports problems as markers, and falls back to a full build the moment the delta is unavailable. Anything fancier should be justified by a requirement, not habit.
Requirements the framework is actually solving
An IDE build has constraints a CI build does not. It runs on every save (when auto-build is on), it shares a process with the editor, and it must finish in well under a second for small edits or developers will disable it. That drives three requirements:
- Minimal work per trigger. Rebuild only what changed and what depends on the change.
- Precise feedback. Errors must appear as markers on the exact file and line, not just in a console log.
- Crash tolerance. The IDE can be killed mid-build; the next session must recover without manual cleanup.
The framework's answer is IncrementalProjectBuilder (from org.eclipse.core.resources). Each project registers one or more builders in its .project file, and the platform calls build() with a kind: FULL_BUILD, INCREMENTAL_BUILD, AUTO_BUILD, or CLEAN_BUILD. The builder decides the work set; the platform only tells it what happened.
The smallest suitable design: consume the delta
The core API call is getDelta(getProject()), which returns an IResourceDelta — a tree describing every resource added, removed, or changed since this builder last ran. The minimal design is a visitor over that tree:
protected IProject[] build(int kind, Map<String,String> args, IProgressMonitor monitor) {
if (kind == FULL_BUILD) {
fullBuild(monitor);
} else {
IResourceDelta delta = getDelta(getProject());
if (delta == null) {
fullBuild(monitor); // delta lost: crash or external change
} else {
delta.accept(new DeltaVisitor()); // visit only changed resources
}
}
return null; // or projects whose deltas you also need
}Two things make this design stay small. First, do not build your own cross-file dependency tracker if you can avoid it. JDT, for example, maintains its own dependency graph and persists build state on disk between sessions, so changing one Java file recompiles only the dependents JDT already knows about. A custom builder that shells out to a compiler should similarly record "output X came from inputs Y, Z" in its own state file under the project's metadata area, and re-run only when an input changed. Second, mark every generated file as derived (IResource.setDerived(true)) so team providers, searches, and other builders ignore it. Outputs sitting unmarked inside source folders get reprocessed — a classic cause of build loops.
Trust and data boundaries
Builders execute inside the IDE process with full access to the workspace. That sets the boundaries:
- File contents are untrusted input. Annotation processors and code generators parse whatever a developer (or a pulled branch) left on disk. A malformed source file must produce a marker, not an unhandled exception that aborts the build.
- The filesystem is not stable between builds. Files can change outside Eclipse — a
git checkout, a script, an editor. After a refresh, the platform may have no coherent delta for your builder, which is exactly whengetDelta()returnsnull. Treat null as "assume everything changed." - Your persisted state can lie. If your builder caches a dependency graph, version it. When the compiler version or a config file changes, discard the cache and request a full build rather than trusting stale entries.
Operational checks
These are the checks worth wiring into development and support, with what a healthy result looks like:
- Confirm registration and order. Open the project's
.projectfile and verify your builder's ID appears inbuildSpecin the intended order relative toorg.eclipse.jdt.core.javabuilder. Order matters when one builder consumes another's output. - Verify incrementality. Touch one source file, trigger an incremental build, and check timestamps (or your builder's trace logging) to confirm only affected outputs were regenerated. If everything rebuilds, your delta visitor is falling through to full-build logic.
- Exercise the null-delta path. Delete a generated input outside Eclipse, refresh the project (F5), and build. Confirm the builder performs a full build instead of failing or skipping.
- Check marker fidelity. Introduce a compile error and confirm an
IMarkerappears in the Problems view with the correct file and line, then disappears after the fix and rebuild. Markers that survive their fix usually mean the builder never deletes old markers before reprocessing — callresource.deleteMarkers(...)at the start of each unit of work.
Failure modes to design for
- Stale derived artifacts after interruption. A killed JVM can leave half-written outputs. Mitigate by writing to a temp file and moving it into place, or by making the next build cheap enough that a full rebuild is an acceptable recovery.
- Delta loss. Covered above: crash or out-of-workspace change means
nulldelta. If your full build takes minutes, this is the failure users will actually complain about. - Builder ordering conflicts. Two builders on one project where A generates sources B compiles must run A first; getting this wrong produces errors that vanish on the second build — a tell-tale symptom.
- Auto-build drag. On large workspaces, building on every save makes the UI sluggish, so teams disable auto-build. That does not break correctness, but it shifts staleness risk to the developer, who may now run tests against hour-old class files.
Conditions that should change the design
The IDE-centric model has a hard boundary: workspace build state lives in Eclipse metadata and is not portable. If you need reproducible builds, headless CI, or builds developers can run from a terminal, mirror the build with an external system — Maven via m2e or Gradle via Buildship — and treat the Eclipse builder as a fast feedback layer, not the source of truth. Similarly, if your build must consume inputs from outside the workspace or produce signed artifacts, move that work out of the builder entirely; the incremental framework optimizes for edit-compile-debug speed, not release engineering.
Behavior details — delta availability rules, build triggers, API signatures — vary across Eclipse Platform releases, so verify against the API documentation for your target release before relying on specifics. The design shape, though, has been stable for years: delta in, markers out, full build as the honest fallback.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.