WatchService only watches a single directory level. How do you watch an entire directory tree, and what edge cases must you handle?
answer
- No recursive flag — walk + register every directory
- Files.walkFileTree + preVisitDirectory to register
- Keep Map<WatchKey, Path> to resolve contexts
- On ENTRY_CREATE of a dir → re-walk and register it
- Watch out for create/populate race, symlink loops, inotify limits
basics
~20 sWatchService doesn't watch subfolders automatically. To watch a whole tree, you walk the directory tree once and register every directory, keep a map from each WatchKey to its directory, and whenever a new subdirectory is created (an ENTRY_CREATE you observe) you register that one too.
solid answer
~50 sBecause registration is single-level, watching a tree means doing the recursion yourself. The standard approach: use Files.walkFileTree (or walk) to visit every directory under the root and call register on each, storing the returned WatchKey in a Map<WatchKey, Path> so you can resolve each event's relative context back to an absolute path. Then in your event loop, when you receive an ENTRY_CREATE whose resolved target is itself a directory, recursively walk and register it (and its children, to cover the race where the subtree was populated before you registered). Edge cases: directories created and filled before you can register them (re-walk on create to catch missed children), directories deleted (reset() returns false → remove the key from your map and let it go invalid), rapid create/delete churn, symlink loops (decide whether to follow links to avoid cycles), and the cost/limits of many registrations on large trees (e.g. inotify watch limits on Linux). OVERFLOW handling still applies per directory.
go deeper
Knows WatchService isn't recursive and that you'd have to register subdirectories yourself.
Can implement the walk-and-register approach with a key→dir map and register new directories seen via ENTRY_CREATE.
Handles the create/populate race by re-walking, cleans up on invalid keys, addresses symlink cycles and OS watch limits, and correlates moves as delete+create.
Evaluates whether tree-watching is the right architecture at scale, designs reconciliation/backpressure, and chooses between native watching, periodic scanning, or a dedicated library based on churn, size, and reliability needs.
## Why recursion is your job `Path.register(watchService, kinds...)` registers exactly **one directory**. Events fire only for entries directly inside it — not for entries inside its subdirectories. There is no `RECURSIVE` flag in the standard API. So watching a tree is an application-level pattern built on top of single-level registration. ## The core pattern ### 1. Register the whole existing tree up front Walk the tree and register every directory you find: ```java Map<WatchKey, Path> keys = new HashMap<>(); void registerAll(Path start, WatchService ws) throws IOException { Files.walkFileTree(start, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes a) throws IOException { WatchKey key = dir.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); keys.put(key, dir); return FileVisitResult.CONTINUE; } }); } ``` `Files.walkFileTree` is the NIO.2 tree walker; `SimpleFileVisitor.preVisitDirectory` is called for each directory, which is where we register. We store `key → dir` in a **map** because a WatchEvent's `context()` is only the file name relative to *its* directory — to build an absolute path you need to know which directory the signalling key belongs to. ### 2. Resolve events using the map ```java WatchKey key = ws.take(); Path dir = keys.get(key); for (WatchEvent<?> e : key.pollEvents()) { Path name = (Path) e.context(); Path child = dir.resolve(name); // absolute path ... } ``` ### 3. Register newly created subdirectories When a new directory appears, you must start watching it too — otherwise its contents are invisible: ```java if (e.kind() == ENTRY_CREATE && Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { registerAll(child, ws); // walk + register the new subtree } ``` Note we **re-walk** the new directory rather than registering just it. This handles the **race**: between the directory being created and you registering it, files (or sub-subdirectories) may already have been added; walking the subtree catches those you'd otherwise miss the events for. ### 4. Clean up deleted directories When `key.reset()` returns false, the directory is gone or inaccessible: ```java if (!key.reset()) { keys.remove(key); if (keys.isEmpty()) break; // nothing left to watch } ``` ## Edge cases and gotchas - **Create-then-populate race (above):** always re-walk a newly created directory, not just register it, or you lose events for pre-existing children. - **Deleted directories:** their keys become invalid; remove them from the map on a false reset to avoid leaks and stale lookups. - **Symlink cycles:** if you follow symbolic links while walking, a link pointing back up the tree creates an infinite loop. Decide explicitly whether to follow links; `walkFileTree` does not follow them by default (you must pass `FileVisitOption.FOLLOW_LINKS`), and if you do you must guard against cycles. - **Scale / OS limits:** each registered directory consumes a native watch. On Linux, `inotify` caps the number of watches (`max_user_watches`) and instances; watching a tree with tens of thousands of directories can exhaust these, causing registration to fail. Large trees may need a different strategy or raised limits. - **High churn:** rapid create/delete of directories can produce bursts and OVERFLOW; per-directory OVERFLOW handling and reconciliation still apply. - **Move/rename:** a move within the tree typically appears as DELETE in the source directory and CREATE in the destination; there is no atomic 'moved' event, so you correlate them yourself if needed. ## When to step back If you find yourself fighting watch limits, symlink cycles, and burst overflow on a huge, hot tree, consider whether watching is even the right tool — periodic reconciliation, an explicit upload/notification API, or a purpose-built library may be more robust at scale.
- Why register a Map<WatchKey, Path> instead of just using the event's context?context() is only the file name relative to its own directory; without knowing which directory the signalling key watches you can't build the absolute path, so you map each key back to its directory.
- Why re-walk a newly created directory instead of just calling register on it?Between creation and registration, files or sub-directories may already have been added; walking the new subtree catches those children whose events you would otherwise miss due to the race.
- What risk does following symbolic links during the walk introduce?A symlink pointing back up the tree creates a cycle, causing infinite recursion/registration; you must either not follow links or detect and break cycles.
It's like hiring one security guard per room: to cover a whole building you must station a guard in every room, and post a new guard the moment a new room is built.
saying these in an interview costs you the question
- Expecting subdirectories to be watched automatically
- Registering a new directory without re-walking it (missing the create/populate race)
- Using context() as an absolute path with no key→dir map
- Following symlinks without cycle protection
- Ignoring inotify/OS watch limits on very large trees