Act on files the moment they've finished landing.
PathWatcher watches a directory for created, modified and removed files — and tells you when a file is completely written, so you never grab a half-finished upload. Native JDK events where the OS allows, automatic polling on macOS. One dependency, nothing to install.
"Created" is not the same as "finished".
The moment a file appears is the moment it's least safe to touch — it may still be uploading. The classic bug is an ingestion job that reads a file mid-write and chokes on half a row. PathWatcher solves exactly that.
A CREATE event fires too early
Raw filesystem APIs tell you a file was created, not that it's done. Act on the create and you race the writer — truncated reads, partial parses, corrupted imports.
A COMPLETELY_CREATED event fires when it's done
A completion worker watches each new file settle — by timeout or by retry — and only then raises FILE_COMPLETELY_CREATED. Register a handler and you receive files you can safely open, every time.
Watch a folder in a handful of lines.
Point it at a directory, register the handlers you care about, start. It picks the right scanner mode for the OS on its own.
- 01
Add the dependency
Maven or Gradle, from the Cantara repository.
- 02
Watch & register handlers
Created, modified, removed — and completely-created for safe processing.
- 03
Start
Native WatchService where available, polling on macOS — chosen automatically.
pom.xml
<!-- repository: https://mvnrepo.cantara.no --> <dependency> <groupId>no.cantara.pathwatcher</groupId> <artifactId>pathwatch</artifactId> <version>1.71</version> </dependency>
Ingest.java
PathWatcher pw = PathWatcher.getInstance(); pw.watch(Path.of("/srv/inbox")); // fires only once the file is fully written and closed pw.registerFileCompletelyCreatedHandler(event -> { Path file = event.getFile(); ingest(file); // safe: no partial reads }); pw.registerRemovedHandler(event -> log.info("gone: {}", event.getFile())); pw.start();
Producer, consumer, and a completion worker.
A producer discovers filesystem changes and queues them; a consumer drains the queue, waits for new files to settle, and invokes your callbacks. The scanner mode is chosen for the platform.
Native or poll
JDK WatchService for event-driven I/O; polling where native events aren't available (macOS).
Discover changes
Created, modified and removed files are detected and queued on a worker thread.
Wait for completion
A completion worker confirms a new file has stopped growing — by timeout or by retries.
Invoke handlers
The consumer thread calls your registered callbacks with a FileWatchEvent.
Everything a drop-folder pipeline needs.
Lifecycle events, safe-to-process completion, and cross-platform behaviour — with sensible defaults.
Created · modified · removed
Register a handler per lifecycle event; each fires with a FileWatchEvent carrying the path and file attributes.
Completely-created
The headline feature: know when a file is fully written and closed before you open it — no more partial reads.
Native & poll, auto
Uses native WatchService when the JDK/OS supports it, and falls back to polling on macOS automatically.
Existing files at startup
setScanForExistingFilesAtStartup(true) replays files already in the directory as create events on start.
Completion modes
Choose TIMEOUT or RETRIES and set the interval, to match how your writers behave.
Just the JDK & a queue
Event-driven I/O on worker threads, one small dependency footprint, no native binaries or host agents.
From discovered to safe-to-process.
Every watched file moves through a small set of states. The one you usually wait for is COMPLETED — surfaced as the completely-created event.
| State | Meaning | Event you receive |
|---|---|---|
| DISCOVERED | A new path has appeared in the watched directory. | FILE_CREATED |
| INCOMPLETE | The file is still being written — not yet safe to read. | FILE_MODIFY |
| COMPLETED | Writing has settled; the file is fully written and closed. | FILE_COMPLETELY_CREATED |
| REMOVED | The path was deleted from the watched directory. | FILE_REMOVED |
Where it earns its keep.
Drop-folder ETL
An upstream system drops CSVs or XML into a directory. Process each one exactly when it's finished — never mid-write.
Large files, safely
A 2 GB upload takes seconds to appear and minutes to finish. Wait for completely-created and skip the truncated-read bug entirely.
Legacy handoff by folder
Many systems still integrate by writing to a shared directory. PathWatcher turns that folder into a clean event stream.
Catch up on what's waiting
Replay files already sitting in the directory at boot, so nothing that arrived while you were down gets missed.
WatchService with the sharp edges filed off.
The JDK gives you raw filesystem events. PathWatcher adds the parts you'd otherwise write yourself — completion detection, macOS polling, and a clean handler API — and stays a small library.
| PathWatcher | java.nio WatchService | commons-io monitor | inotify / fswatch | |
|---|---|---|---|---|
| Created / modified / removed events | ● | ● | ● | ● |
| "File completely written" signal | ● | — | — | — |
| Works on macOS out of the box | ● | ◐ | ● | ◐ |
| Clean per-event handler API | ● | — | ● | — |
| Pure JVM, no external tools | ● | ● | ● | — |
| Replay existing files at startup | ● | — | ◐ | — |
Honest positioning: if all you need is a raw create/modify/delete event, plain WatchService or commons-io will do. PathWatcher's reason to exist is the completely-created signal plus consistent macOS behaviour — the boilerplate every drop-folder pipeline ends up writing. It watches a single directory by design; for many independent trees, run several watchers or reach for a heavier framework.
What to keep in mind.
Stable and in active use — with a few deliberate boundaries worth knowing up front.
One watch directory. A single instance watches one watchDir. Run multiple watchers for multiple trees.
Callbacks aren't thread-safe. Handlers run on the consumer thread; guard any shared state you touch from them.
macOS uses polling. The JDK has no native watch events on macOS, so PathWatcher polls there — slightly higher latency by design.
Completion is heuristic. "Done writing" is inferred by settling (timeout/retries); tune the mode to how your writers actually behave.
Turn a directory into a clean event stream.
Add the dependency, watch a folder, and start receiving files you can safely open. That's the whole idea.