skip to content

WatchService only watches a single directory level. How do you watch an entire directory tree, and what edge cases must you handle?

level: seniorimportance: should knowfreq 40%

answer

  1. No recursive flag — walk + register every directory
  2. Files.walkFileTree + preVisitDirectory to register
  3. Keep Map<WatchKey, Path> to resolve contexts
  4. On ENTRY_CREATE of a dir → re-walk and register it
  5. Watch out for create/populate race, symlink loops, inotify limits

basics

~20 s

WatchService 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 s

Because 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

for a junior

Knows WatchService isn't recursive and that you'd have to register subdirectories yourself.

for a middle

Can implement the walk-and-register approach with a key→dir map and register new directories seen via ENTRY_CREATE.

for a senior

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.

for a principal

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

context