diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml
index 07727e4c..2e2882c9 100644
--- a/.github/workflows/publish-central.yml
+++ b/.github/workflows/publish-central.yml
@@ -36,3 +36,4 @@ jobs:
           MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
           MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
           MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
+          MAVEN_GPG_KEY_FINGERPRINT: "58117AFA1F85B3EEC154677D615D449FE6E6A235"
\ No newline at end of file
diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml
index 59b312e4..5980df5c 100644
--- a/.github/workflows/publish-github.yml
+++ b/.github/workflows/publish-github.yml
@@ -23,6 +23,7 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
           MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
+          MAVEN_GPG_KEY_FINGERPRINT: "58117AFA1F85B3EEC154677D615D449FE6E6A235"
       - name: Slack Notification
         uses: rtCamp/action-slack-notify@v2
         env:
diff --git a/pom.xml b/pom.xml
index a0fc375d..79aaa787 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptofs</artifactId>
-	<version>2.7.0</version>
+	<version>2.7.1</version>
 	<name>Cryptomator Crypto Filesystem</name>
 	<description>This library provides the Java filesystem provider used by Cryptomator.</description>
 	<url>https://github.com/cryptomator/cryptofs</url>
@@ -23,19 +23,30 @@
 		<dagger.version>2.51.1</dagger.version>
 		<guava.version>33.2.1-jre</guava.version>
 		<caffeine.version>3.1.8</caffeine.version>
-		<slf4j.version>2.0.13</slf4j.version>
+		<slf4j.version>2.0.16</slf4j.version>
 
 		<!-- test dependencies -->
-		<junit.jupiter.version>5.10.3</junit.jupiter.version>
-		<mockito.version>5.12.0</mockito.version>
+		<junit.jupiter.version>5.11.3</junit.jupiter.version>
+		<mockito.version>5.14.2</mockito.version>
 		<hamcrest.version>3.0</hamcrest.version>
 		<jimfs.version>1.3.0</jimfs.version>
 
 		<!-- build plugin dependencies -->
-		<dependency-check.version>10.0.3</dependency-check.version>
+		<mvn-compiler.version>3.13.0</mvn-compiler.version>
+		<mvn-dependency.version>3.7.1</mvn-dependency.version>
+		<mvn-surefire.version>3.5.1</mvn-surefire.version>
+		<mvn-jar.version>3.4.2</mvn-jar.version>
+		<mvn-source.version>3.3.1</mvn-source.version>
+		<mvn-javadoc.version>3.10.1</mvn-javadoc.version>
+		<mvn-gpg.version>3.2.7</mvn-gpg.version>
+
+		<dependency-check.version>11.0.0</dependency-check.version>
 		<junit-tree-reporter.version>1.3.0</junit-tree-reporter.version>
 		<jacoco.version>0.8.12</jacoco.version>
 		<nexus-staging.version>1.7.0</nexus-staging.version>
+
+		<!-- Property used by surefire to determine jacoco engine -->
+		<surefire.jacoco.args></surefire.jacoco.args>
 	</properties>
 
 	<licenses>
@@ -143,7 +154,7 @@
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-compiler-plugin</artifactId>
-				<version>3.13.0</version>
+				<version>${mvn-compiler.version}</version>
 				<configuration>
 					<showWarnings>true</showWarnings>
 					<annotationProcessorPaths>
@@ -155,10 +166,23 @@
 					</annotationProcessorPaths>
 				</configuration>
 			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-dependency-plugin</artifactId>
+				<version>${mvn-dependency.version}</version>
+				<executions>
+					<execution>
+						<id>jar-paths-to-properties</id>
+						<goals>
+							<goal>properties</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-surefire-plugin</artifactId>
-				<version>3.3.1</version>
+				<version>${mvn-surefire.version}</version>
 				<dependencies>
 					<dependency>
 						<groupId>me.fabriciorby</groupId>
@@ -176,16 +200,17 @@
 					<statelessTestsetInfoReporter
 							implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter">
 					</statelessTestsetInfoReporter>
+					<argLine>@{surefire.jacoco.args} -javaagent:${net.bytebuddy:byte-buddy-agent:jar}</argLine>
 				</configuration>
 			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-jar-plugin</artifactId>
-				<version>3.4.2</version>
+				<version>${mvn-jar.version}</version>
 			</plugin>
 			<plugin>
 				<artifactId>maven-source-plugin</artifactId>
-				<version>3.3.1</version>
+				<version>${mvn-source.version}</version>
 				<executions>
 					<execution>
 						<id>attach-sources</id>
@@ -197,7 +222,7 @@
 			</plugin>
 			<plugin>
 				<artifactId>maven-javadoc-plugin</artifactId>
-				<version>3.8.0</version>
+				<version>${mvn-javadoc.version}</version>
 				<executions>
 					<execution>
 						<id>attach-javadocs</id>
@@ -281,6 +306,9 @@
 								<goals>
 									<goal>prepare-agent</goal>
 								</goals>
+								<configuration>
+									<propertyName>surefire.jacoco.args</propertyName>
+								</configuration>
 							</execution>
 							<execution>
 								<id>report</id>
@@ -300,7 +328,7 @@
 				<plugins>
 					<plugin>
 						<artifactId>maven-gpg-plugin</artifactId>
-						<version>3.2.4</version>
+						<version>${mvn-gpg.version}</version>
 						<executions>
 							<execution>
 								<id>sign-artifacts</id>
diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java b/src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java
new file mode 100644
index 00000000..699f03db
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java
@@ -0,0 +1,82 @@
+package org.cryptomator.cryptofs;
+
+import com.github.benmanes.caffeine.cache.AsyncCache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Caches for the cleartext path of a directory its ciphertext path to the content directory.
+ */
+public class CiphertextDirCache {
+
+	private static final int MAX_CACHED_PATHS = 5000;
+	private static final Duration MAX_CACHE_AGE = Duration.ofSeconds(20);
+
+	private final AsyncCache<CryptoPath, CiphertextDirectory> ciphertextDirectories = Caffeine.newBuilder() //
+			.maximumSize(MAX_CACHED_PATHS) //
+			.expireAfterWrite(MAX_CACHE_AGE) //
+			.buildAsync();
+
+	/**
+	 * Removes all (key,value) entries, where {@code key.startsWith(oldPrefix) == true}.
+	 *
+	 * @param basePrefix The prefix key which the keys are checked against
+	 */
+	void removeAllKeysWithPrefix(CryptoPath basePrefix) {
+		ciphertextDirectories.asMap().keySet().removeIf(p -> p.startsWith(basePrefix));
+	}
+
+	/**
+	 * Remaps all (key,value) entries, where {@code key.startsWith(oldPrefix) == true}.
+	 * The new key is computed by replacing the oldPrefix with the newPrefix.
+	 *
+	 * @param oldPrefix the prefix key which the keys are checked against
+	 * @param newPrefix the prefix key which replaces {@code oldPrefix}
+	 */
+	void recomputeAllKeysWithPrefix(CryptoPath oldPrefix, CryptoPath newPrefix) {
+		var remappedEntries = new ArrayList<CacheEntry>();
+		ciphertextDirectories.asMap().entrySet().removeIf(e -> {
+			if (e.getKey().startsWith(oldPrefix)) {
+				var remappedPath = newPrefix.resolve(oldPrefix.relativize(e.getKey()));
+				return remappedEntries.add(new CacheEntry(remappedPath, e.getValue()));
+			} else {
+				return false;
+			}
+		});
+		remappedEntries.forEach(e -> ciphertextDirectories.put(e.clearPath(), e.cipherDir()));
+	}
+
+
+	/**
+	 * Gets the cipher directory for the given cleartext path. If a cache miss occurs, the mapping is loaded with the {@code ifAbsent} function.
+	 * @param cleartextPath Cleartext path key
+	 * @param ifAbsent Function to compute the (cleartextPath, cipherDir) mapping on a cache miss.
+	 * @return a {@link CiphertextDirectory}, containing the dirId and the ciphertext content directory path
+	 * @throws IOException if the loading function throws an IOException
+	 */
+	CiphertextDirectory get(CryptoPath cleartextPath, CipherDirLoader ifAbsent) throws IOException {
+		var futureMapping = new CompletableFuture<CiphertextDirectory>();
+		var currentMapping = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, futureMapping);
+		if (currentMapping != null) {
+			return currentMapping.join();
+		} else {
+			futureMapping.complete(ifAbsent.load());
+			return futureMapping.join();
+		}
+	}
+
+	@FunctionalInterface
+	interface CipherDirLoader {
+
+		CiphertextDirectory load() throws IOException;
+	}
+
+	private record CacheEntry(CryptoPath clearPath, CompletableFuture<CiphertextDirectory> cipherDir) {
+
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectory.java b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectory.java
new file mode 100644
index 00000000..4dc8f025
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectory.java
@@ -0,0 +1,21 @@
+package org.cryptomator.cryptofs;
+
+import java.nio.file.Path;
+import java.util.Objects;
+
+//own file due to dagger
+
+/**
+ * Represents a ciphertext directory without it's mount point in the virtual filesystem.
+ *
+ * @param dirId The (ciphertext) dir id (not encrypted, just a uuid)
+ * @param path The path to content directory (which contains the actual encrypted files and links to subdirectories)
+ */
+public record CiphertextDirectory(String dirId, Path path) {
+
+	public CiphertextDirectory(String dirId, Path path) {
+		this.dirId = Objects.requireNonNull(dirId);
+		this.path = Objects.requireNonNull(path);
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index d3d7af7c..b0c67943 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -8,7 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs;
 
-import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
 import org.cryptomator.cryptofs.attr.AttributeByNameProvider;
 import org.cryptomator.cryptofs.attr.AttributeProvider;
 import org.cryptomator.cryptofs.attr.AttributeViewProvider;
@@ -142,7 +141,7 @@ public Path getCiphertextPath(Path cleartextPath) throws IOException {
 		var p = CryptoPath.castAndAssertAbsolute(cleartextPath);
 		var nodeType = cryptoPathMapper.getCiphertextFileType(p);
 		if (nodeType == CiphertextFileType.DIRECTORY) {
-			return cryptoPathMapper.getCiphertextDir(p).path;
+			return cryptoPathMapper.getCiphertextDir(p).path();
 		}
 		var cipherFile = cryptoPathMapper.getCiphertextFilePath(p);
 		if (nodeType == CiphertextFileType.SYMLINK) {
@@ -316,22 +315,22 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
 		if (cleartextParentDir == null) {
 			return;
 		}
-		Path ciphertextParentDir = cryptoPathMapper.getCiphertextDir(cleartextParentDir).path;
+		Path ciphertextParentDir = cryptoPathMapper.getCiphertextDir(cleartextParentDir).path();
 		if (!Files.exists(ciphertextParentDir)) {
 			throw new NoSuchFileException(cleartextParentDir.toString());
 		}
 		cryptoPathMapper.assertNonExisting(cleartextDir);
 		CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir);
 		Path ciphertextDirFile = ciphertextPath.getDirFilePath();
-		CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
+		var ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
 		// atomically check for FileAlreadyExists and create otherwise:
 		Files.createDirectory(ciphertextPath.getRawPath());
 		try (FileChannel channel = FileChannel.open(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), attrs)) {
-			channel.write(UTF_8.encode(ciphertextDir.dirId));
+			channel.write(UTF_8.encode(ciphertextDir.dirId()));
 		}
 		// create dir if and only if the dirFile has been created right now (not if it has been created before):
 		try {
-			Files.createDirectories(ciphertextDir.path);
+			Files.createDirectories(ciphertextDir.path());
 			dirIdBackup.execute(ciphertextDir);
 			ciphertextPath.persistLongFileName();
 		} catch (IOException e) {
@@ -432,7 +431,7 @@ private void deleteFileOrSymlink(CiphertextFilePath ciphertextPath) throws IOExc
 	}
 
 	private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException {
-		Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path;
+		Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path();
 		Path ciphertextDirFile = ciphertextPath.getDirFilePath();
 		try {
 			ciphertextDirDeleter.deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDir, cleartextPath);
@@ -505,7 +504,7 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
 			ciphertextTarget.persistLongFileName();
 		} else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
 			// keep existing (if empty):
-			Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
+			Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path();
 			try (DirectoryStream<Path> ds = Files.newDirectoryStream(ciphertextTargetDir)) {
 				if (ds.iterator().hasNext()) {
 					throw new DirectoryNotEmptyException(cleartextTarget.toString());
@@ -515,8 +514,8 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
 			throw new FileAlreadyExistsException(cleartextTarget.toString(), null, "Ciphertext file already exists: " + ciphertextTarget);
 		}
 		if (ArrayUtils.contains(options, StandardCopyOption.COPY_ATTRIBUTES)) {
-			Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path;
-			Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
+			Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path();
+			Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path();
 			copyAttributes(ciphertextSourceDir, ciphertextTargetDir);
 		}
 	}
@@ -622,7 +621,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
 				throw new AtomicMoveNotSupportedException(cleartextSource.toString(), cleartextTarget.toString(), "Replacing directories during move requires non-atomic status checks.");
 			}
 			// check if dir is empty:
-			Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
+			Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path();
 			boolean targetCiphertextDirExists = true;
 			try (DirectoryStream<Path> ds = Files.newDirectoryStream(targetCiphertextDirContentDir, DirectoryStreamFilters.EXCLUDE_DIR_ID_BACKUP)) {
 				if (ds.iterator().hasNext()) {
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 3fffd21c..1360d6d7 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -155,7 +155,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 			Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
 			Files.createDirectories(vaultCipherRootPath);
 			// create dirId backup:
-			DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath));
+			DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath));
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
index c725611e..01633d38 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
@@ -8,7 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs;
 
-import com.github.benmanes.caffeine.cache.AsyncCache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.LoadingCache;
 import com.google.common.io.BaseEncoding;
@@ -27,10 +26,7 @@
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.time.Duration;
-import java.util.Objects;
 import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
 
 import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME;
 
@@ -39,8 +35,6 @@ public class CryptoPathMapper {
 
 	private static final Logger LOG = LoggerFactory.getLogger(CryptoPathMapper.class);
 	private static final int MAX_CACHED_CIPHERTEXT_NAMES = 5000;
-	private static final int MAX_CACHED_DIR_PATHS = 5000;
-	private static final Duration MAX_CACHE_AGE = Duration.ofSeconds(20);
 
 	private final Cryptor cryptor;
 	private final Path dataRoot;
@@ -48,7 +42,7 @@ public class CryptoPathMapper {
 	private final LongFileNameProvider longFileNameProvider;
 	private final VaultConfig vaultConfig;
 	private final LoadingCache<DirIdAndName, String> ciphertextNames;
-	private final AsyncCache<CryptoPath, CiphertextDirectory> ciphertextDirectories;
+	private final CiphertextDirCache ciphertextDirCache;
 
 	private final CiphertextDirectory rootDirectory;
 
@@ -60,7 +54,7 @@ public class CryptoPathMapper {
 		this.longFileNameProvider = longFileNameProvider;
 		this.vaultConfig = vaultConfig;
 		this.ciphertextNames = Caffeine.newBuilder().maximumSize(MAX_CACHED_CIPHERTEXT_NAMES).build(this::getCiphertextFileName);
-		this.ciphertextDirectories = Caffeine.newBuilder().maximumSize(MAX_CACHED_DIR_PATHS).expireAfterWrite(MAX_CACHE_AGE).buildAsync();
+		this.ciphertextDirCache = new CiphertextDirCache();
 		this.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID);
 	}
 
@@ -69,7 +63,7 @@ public class CryptoPathMapper {
 	 *
 	 * @param cleartextPath A path
 	 * @throws FileAlreadyExistsException If the node exists
-	 * @throws IOException                If any I/O error occurs while attempting to resolve the ciphertext path
+	 * @throws IOException If any I/O error occurs while attempting to resolve the ciphertext path
 	 */
 	public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExistsException, IOException {
 		try {
@@ -121,7 +115,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws
 		}
 		CiphertextDirectory parent = getCiphertextDir(parentPath);
 		String cleartextName = cleartextPath.getFileName().toString();
-		return getCiphertextFilePath(parent.path, parent.dirId, cleartextName);
+		return getCiphertextFilePath(parent.path(), parent.dirId(), cleartextName);
 	}
 
 	public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) {
@@ -136,36 +130,36 @@ public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String
 	}
 
 	private String getCiphertextFileName(DirIdAndName dirIdAndName) {
-		return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
+		return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.clearNodeName(), dirIdAndName.dirId().getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
 	}
 
+	/**
+	 * Removes the given cleartext path and all cached child paths from the dir cache
+	 * @param cleartextPath the root cleartext path, for which all mappings starting with it will be removed
+	 */
 	public void invalidatePathMapping(CryptoPath cleartextPath) {
-		ciphertextDirectories.asMap().remove(cleartextPath);
+		ciphertextDirCache.removeAllKeysWithPrefix(cleartextPath);
 	}
 
+	/**
+	 * Moves the given cleartext path and all cached child paths in the dir cache
+	 * @param cleartextSrc the root cleartext path, for which alle mappings starting with it will be moved
+	 * @param cleartextDst the destination cleartext path. The path itself and all childs will be adjusted to start with cleartextDst.
+	 */
 	public void movePathMapping(CryptoPath cleartextSrc, CryptoPath cleartextDst) {
-		var cachedValue = ciphertextDirectories.asMap().remove(cleartextSrc);
-		if (cachedValue != null) {
-			ciphertextDirectories.put(cleartextDst, cachedValue);
-		}
+		ciphertextDirCache.recomputeAllKeysWithPrefix(cleartextSrc, cleartextDst);
 	}
 
 	public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException {
 		assert cleartextPath.isAbsolute();
-		CryptoPath parentPath = cleartextPath.getParent();
-		if (parentPath == null) {
+		if (cleartextPath.getParent() == null) {
 			return rootDirectory;
 		} else {
-			var lazyEntry = new CompletableFuture<CiphertextDirectory>();
-			var priorEntry = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, lazyEntry);
-			if (priorEntry != null) {
-				return priorEntry.join();
-			} else {
+			CiphertextDirCache.CipherDirLoader cipherDirLoaderIfAbsent = () -> {
 				Path dirFile = getCiphertextFilePath(cleartextPath).getDirFilePath();
-				CiphertextDirectory cipherDir = resolveDirectory(dirFile);
-				lazyEntry.complete(cipherDir);
-				return cipherDir;
-			}
+				return resolveDirectory(dirFile);
+			};
+			return ciphertextDirCache.get(cleartextPath, cipherDirLoaderIfAbsent);
 		}
 	}
 
@@ -179,57 +173,4 @@ private CiphertextDirectory resolveDirectory(String dirId) {
 		Path dirPath = dataRoot.resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
 		return new CiphertextDirectory(dirId, dirPath);
 	}
-
-	public static class CiphertextDirectory {
-		public final String dirId;
-		public final Path path;
-
-		public CiphertextDirectory(String dirId, Path path) {
-			this.dirId = Objects.requireNonNull(dirId);
-			this.path = Objects.requireNonNull(path);
-		}
-
-		@Override
-		public int hashCode() {
-			return Objects.hash(dirId, path);
-		}
-
-		@Override
-		public boolean equals(Object obj) {
-			if (obj == this) {
-				return true;
-			} else if (obj instanceof CiphertextDirectory other) {
-				return this.dirId.equals(other.dirId) && this.path.equals(other.path);
-			} else {
-				return false;
-			}
-		}
-	}
-
-	private static class DirIdAndName {
-		public final String dirId;
-		public final String name;
-
-		public DirIdAndName(String dirId, String name) {
-			this.dirId = Objects.requireNonNull(dirId);
-			this.name = Objects.requireNonNull(name);
-		}
-
-		@Override
-		public int hashCode() {
-			return Objects.hash(dirId, name);
-		}
-
-		@Override
-		public boolean equals(Object obj) {
-			if (obj == this) {
-				return true;
-			} else if (obj instanceof DirIdAndName other) {
-				return this.dirId.equals(other.dirId) && this.name.equals(other.name);
-			} else {
-				return false;
-			}
-		}
-	}
-
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/DirIdAndName.java b/src/main/java/org/cryptomator/cryptofs/DirIdAndName.java
new file mode 100644
index 00000000..8367e556
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/DirIdAndName.java
@@ -0,0 +1,19 @@
+package org.cryptomator.cryptofs;
+
+import java.util.Objects;
+
+//own file due to dagger
+
+/**
+ * Helper object to store the dir id of a directory along with its cleartext name (aka, the last element in the cleartext path)
+ * @param dirId
+ * @param clearNodeName
+ */
+record DirIdAndName(String dirId, String clearNodeName) {
+
+	public DirIdAndName(String dirId, String clearNodeName) {
+		this.dirId = Objects.requireNonNull(dirId);
+		this.clearNodeName = Objects.requireNonNull(clearNodeName);
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java b/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java
index b3cdc0db..6dc2f7fe 100644
--- a/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java
+++ b/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java
@@ -26,17 +26,17 @@ public DirectoryIdBackup(Cryptor cryptor) {
 	}
 
 	/**
-	 * Performs the backup operation for the given {@link CryptoPathMapper.CiphertextDirectory} object.
+	 * Performs the backup operation for the given {@link CiphertextDirectory} object.
 	 * <p>
-	 * The directory id is written via an encrypting channel to the file {@link CryptoPathMapper.CiphertextDirectory#path}/{@value Constants#DIR_BACKUP_FILE_NAME}.
+	 * The directory id is written via an encrypting channel to the file {@link CiphertextDirectory#path()} /{@value Constants#DIR_BACKUP_FILE_NAME}.
 	 *
 	 * @param ciphertextDirectory The cipher dir object containing the dir id and the encrypted content root
 	 * @throws IOException if an IOException is raised during the write operation
 	 */
-	public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
-		try (var channel = Files.newByteChannel(ciphertextDirectory.path.resolve(Constants.DIR_BACKUP_FILE_NAME), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
+	public void execute(CiphertextDirectory ciphertextDirectory) throws IOException {
+		try (var channel = Files.newByteChannel(ciphertextDirectory.path().resolve(Constants.DIR_BACKUP_FILE_NAME), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
 			 var encryptingChannel = wrapEncryptionAround(channel, cryptor)) {
-			encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId.getBytes(StandardCharsets.US_ASCII)));
+			encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId().getBytes(StandardCharsets.US_ASCII)));
 		}
 	}
 
@@ -44,10 +44,10 @@ public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) th
 	 * Static method to explicitly back up the directory id for a specified ciphertext directory.
 	 *
 	 * @param cryptor The cryptor to be used
-	 * @param ciphertextDirectory A {@link org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory} for which the dirId should be back up'd.
+	 * @param ciphertextDirectory A {@link CiphertextDirectory} for which the dirId should be back up'd.
 	 * @throws IOException when the dirId file already exists, or it cannot be written to.
 	 */
-	public static void backupManually(Cryptor cryptor, CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
+	public static void backupManually(Cryptor cryptor, CiphertextDirectory ciphertextDirectory) throws IOException {
 		new DirectoryIdBackup(cryptor).execute(ciphertextDirectory);
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java
index 82563853..7a91c2a6 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java
@@ -60,7 +60,7 @@ private Path getCiphertextPath(CryptoPath path) throws IOException {
 					yield getCiphertextPath(resolved);
 				}
 			case DIRECTORY:
-				yield pathMapper.getCiphertextDir(path).path;
+				yield pathMapper.getCiphertextDir(path).path();
 			case FILE:
 				yield pathMapper.getCiphertextFilePath(path).getFilePath();
 		};
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java
index b2195161..feff23db 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java
@@ -53,7 +53,7 @@ public <A extends BasicFileAttributes> A readAttributes(CryptoPath cleartextPath
 	private Path getCiphertextPath(CryptoPath path, CiphertextFileType type) throws IOException {
 		return switch (type) {
 			case SYMLINK -> pathMapper.getCiphertextFilePath(path).getSymlinkFilePath();
-			case DIRECTORY -> pathMapper.getCiphertextDir(path).path;
+			case DIRECTORY -> pathMapper.getCiphertextDir(path).path();
 			case FILE -> pathMapper.getCiphertextFilePath(path).getFilePath();
 		};
 	}
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java b/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java
index 2c05b34b..ba1e86ad 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java
@@ -28,7 +28,7 @@ public Stream<Node> process(Node node) {
 		if (Files.isRegularFile(dirFile)) {
 			final Path dirPath;
 			try {
-				dirPath = cryptoPathMapper.resolveDirectory(dirFile).path;
+				dirPath = cryptoPathMapper.resolveDirectory(dirFile).path();
 			} catch (IOException e) {
 				LOG.warn("Broken directory file: " + dirFile, e);
 				return Stream.empty();
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java
index 651d7dff..77ceb6f0 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java
@@ -1,9 +1,9 @@
 package org.cryptomator.cryptofs.dir;
 
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.CryptoFileSystemScoped;
 import org.cryptomator.cryptofs.CryptoPath;
 import org.cryptomator.cryptofs.CryptoPathMapper;
-import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
 import org.cryptomator.cryptofs.common.Constants;
 
 import javax.inject.Inject;
@@ -37,8 +37,8 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex
 			throw new ClosedFileSystemException();
 		}
 		CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
-		DirectoryStream<Path> ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path, this::matchesEncryptedContentPattern);
-		var cleartextDirStream = directoryStreamComponentFactory.create(cleartextDir, ciphertextDir.dirId, ciphertextDirStream, filter, streams::remove).directoryStream();
+		DirectoryStream<Path> ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path(), this::matchesEncryptedContentPattern);
+		var cleartextDirStream = directoryStreamComponentFactory.create(cleartextDir, ciphertextDir.dirId(), ciphertextDirStream, filter, streams::remove).directoryStream();
 		streams.put(cleartextDirStream, ciphertextDirStream);
 		return cleartextDirStream;
 	}
diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java
index 7e59d982..28cd3614 100644
--- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java
+++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java
@@ -1,6 +1,6 @@
 package org.cryptomator.cryptofs.health.dirid;
 
-import org.cryptomator.cryptofs.CryptoPathMapper;
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.DirectoryIdBackup;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.common.Constants;
@@ -51,7 +51,7 @@ void fix(Path pathToVault, Cryptor cryptor) throws IOException {
 		var dirIdHash = cryptor.fileNameCryptor().hashDirectoryId(dirId);
 		Path dirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirIdHash.substring(0, 2)).resolve(dirIdHash.substring(2, 32));
 		Files.createDirectories(dirPath);
-		DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(dirId, dirPath));
+		DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(dirId, dirPath));
 	}
 
 	@Override
diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java
index be480db0..9002ba94 100644
--- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java
+++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java
@@ -1,9 +1,8 @@
 package org.cryptomator.cryptofs.health.dirid;
 
-import org.cryptomator.cryptofs.CryptoPathMapper;
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.DirectoryIdBackup;
 import org.cryptomator.cryptofs.VaultConfig;
-import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.health.api.DiagnosticResult;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.Masterkey;
@@ -30,7 +29,7 @@ public String toString() {
 	//visible for testing
 	void fix(Path pathToVault, Cryptor cryptor) throws IOException {
 		Path absCipherDir = pathToVault.resolve(contentDir);
-		DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(dirId, absCipherDir));
+		DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(dirId, absCipherDir));
 	}
 
 	@Override
diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java
index 505c0dc7..350616a9 100644
--- a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java
+++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java
@@ -1,7 +1,7 @@
 package org.cryptomator.cryptofs.health.dirid;
 
 import com.google.common.io.BaseEncoding;
-import org.cryptomator.cryptofs.CryptoPathMapper;
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.DirectoryIdBackup;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.common.CiphertextFileType;
@@ -115,7 +115,7 @@ private void fix(Path pathToVault, VaultConfig config, Cryptor cryptor) throws I
 		Files.deleteIfExists(orphanedDir.resolve(Constants.DIR_BACKUP_FILE_NAME));
 		try (var nonCryptomatorFiles = Files.newDirectoryStream(orphanedDir)) {
 			for (Path p : nonCryptomatorFiles) {
-				Files.move(p, stepParentDir.path.resolve(p.getFileName()), LinkOption.NOFOLLOW_LINKS);
+				Files.move(p, stepParentDir.path().resolve(p.getFileName()), LinkOption.NOFOLLOW_LINKS);
 			}
 		}
 		Files.delete(orphanedDir);
@@ -154,7 +154,7 @@ Path prepareRecoveryDir(Path pathToVault, FileNameCryptor cryptor) throws IOExce
 	}
 
 	// visible for testing
-	CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryptor cryptor, String clearStepParentDirName) throws IOException {
+	CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryptor cryptor, String clearStepParentDirName) throws IOException {
 		//create "stepparent" directory to move orphaned files to
 		String cipherStepParentDirName = encrypt(cryptor.fileNameCryptor(), clearStepParentDirName, Constants.RECOVERY_DIR_ID);
 		Path cipherStepParentDirFile = cipherRecoveryDir.resolve(cipherStepParentDirName + "/" + Constants.DIR_FILE_NAME);
@@ -169,7 +169,7 @@ CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipher
 		String stepParentDirHash = cryptor.fileNameCryptor().hashDirectoryId(stepParentUUID);
 		Path stepParentDir = dataDir.resolve(stepParentDirHash.substring(0, 2)).resolve(stepParentDirHash.substring(2)).toAbsolutePath();
 		Files.createDirectories(stepParentDir);
-		var stepParentCipherDir = new CryptoPathMapper.CiphertextDirectory(stepParentUUID, stepParentDir);
+		var stepParentCipherDir = new CiphertextDirectory(stepParentUUID, stepParentDir);
 		//only if it does not exist
 		try {
 			DirectoryIdBackup.backupManually(cryptor, stepParentCipherDir);
@@ -215,11 +215,11 @@ String decryptFileName(Path orphanedResource, boolean isShortened, String dirId,
 	}
 
 	// visible for testing
-	void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isShortened, CryptoPathMapper.CiphertextDirectory stepParentDir, FileNameCryptor cryptor, MessageDigest sha1) throws IOException {
-		var newCipherName = encrypt(cryptor, newClearName, stepParentDir.dirId);
+	void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isShortened, CiphertextDirectory stepParentDir, FileNameCryptor cryptor, MessageDigest sha1) throws IOException {
+		var newCipherName = encrypt(cryptor, newClearName, stepParentDir.dirId());
 		if (isShortened) {
 			var deflatedName = BaseEncoding.base64Url().encode(sha1.digest(newCipherName.getBytes(StandardCharsets.UTF_8))) + Constants.DEFLATED_FILE_SUFFIX;
-			Path targetPath = stepParentDir.path.resolve(deflatedName);
+			Path targetPath = stepParentDir.path().resolve(deflatedName);
 			Files.move(oldCipherPath, targetPath);
 
 			//adjust name.c9s
@@ -227,7 +227,7 @@ void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isSh
 				fc.write(ByteBuffer.wrap(newCipherName.getBytes(StandardCharsets.UTF_8)));
 			}
 		} else {
-			Path targetPath = stepParentDir.path.resolve(newCipherName);
+			Path targetPath = stepParentDir.path().resolve(newCipherName);
 			Files.move(oldCipherPath, targetPath);
 		}
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/CiphertextDirCacheTest.java b/src/test/java/org/cryptomator/cryptofs/CiphertextDirCacheTest.java
new file mode 100644
index 00000000..9af0e613
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/CiphertextDirCacheTest.java
@@ -0,0 +1,119 @@
+package org.cryptomator.cryptofs;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public class CiphertextDirCacheTest {
+
+	CiphertextDirCache cache;
+	CryptoPath clearPath;
+	CiphertextDirCache.CipherDirLoader dirLoader;
+
+
+	@BeforeEach
+	public void beforeEach() throws IOException {
+		cache = new CiphertextDirCache();
+		clearPath = Mockito.mock(CryptoPath.class);
+		dirLoader = Mockito.mock(CiphertextDirCache.CipherDirLoader.class);
+		var cipherDir = Mockito.mock(CiphertextDirectory.class);
+		Mockito.when(dirLoader.load()).thenReturn(cipherDir);
+	}
+
+	@Test
+	public void testPuttingNewEntryTriggersLoader() throws IOException {
+		var cipherDir = Mockito.mock(CiphertextDirectory.class);
+		Mockito.when(dirLoader.load()).thenReturn(cipherDir);
+
+		var result = cache.get(clearPath, dirLoader);
+		Assertions.assertEquals(cipherDir, result);
+		Mockito.verify(dirLoader).load();
+	}
+
+	@Test
+	public void testPuttingKnownEntryDoesNotTriggerLoader() throws IOException {
+		Mockito.when(dirLoader.load()).thenReturn(Mockito.mock(CiphertextDirectory.class));
+		var dirLoader2 = Mockito.mock(CiphertextDirCache.CipherDirLoader.class);
+
+		var result = cache.get(clearPath, dirLoader);
+		var result2 = cache.get(clearPath, dirLoader2);
+		Assertions.assertEquals(result2, result);
+		Mockito.verify(dirLoader2, Mockito.never()).load();
+	}
+
+	@Nested
+	public class RemovalTest {
+
+		CryptoPath prefixPath = Mockito.mock(CryptoPath.class);
+
+		@Test
+		public void entryRemovedOnPrefixSuccess() throws IOException {
+			Mockito.when(clearPath.startsWith(prefixPath)).thenReturn(true);
+
+			cache.get(clearPath, dirLoader); //triggers loader
+			cache.removeAllKeysWithPrefix(prefixPath);
+			cache.get(clearPath, dirLoader); //triggers loader
+
+			Mockito.verify(dirLoader, Mockito.times(2)).load();
+		}
+
+		@Test
+		public void entryStaysOnPrefixFailure() throws IOException {
+			Mockito.when(clearPath.startsWith(prefixPath)).thenReturn(false);
+
+			cache.get(clearPath, dirLoader); //triggers loader
+			cache.removeAllKeysWithPrefix(prefixPath);
+			cache.get(clearPath, dirLoader); //does not trigger
+
+			Mockito.verify(dirLoader).load();
+		}
+	}
+
+
+	@Nested
+	public class RemapTest {
+
+		CryptoPath newClearPath;
+		CryptoPath oldPrefixPath;
+		CryptoPath newPrefixPath;
+
+		@BeforeEach
+		public void beforeEach() throws IOException {
+			newClearPath = Mockito.mock(CryptoPath.class);
+			oldPrefixPath = Mockito.mock(CryptoPath.class);
+			newPrefixPath = Mockito.mock(CryptoPath.class);
+			Mockito.when(oldPrefixPath.relativize(Mockito.any(Path.class))).thenReturn(oldPrefixPath);
+			Mockito.when(newPrefixPath.resolve(Mockito.any(Path.class))).thenReturn(newClearPath);
+		}
+
+		@Test
+		public void entryRemappedOnPrefixSuccess() throws IOException {
+			Mockito.when(clearPath.startsWith(oldPrefixPath)).thenReturn(true);
+
+			cache.get(clearPath, dirLoader); //triggers loader
+			cache.recomputeAllKeysWithPrefix(oldPrefixPath, newPrefixPath);
+			cache.get(clearPath, dirLoader); //does trigger
+			cache.get(newClearPath, dirLoader); //does not trigger
+
+			Mockito.verify(dirLoader, Mockito.times(2)).load();
+		}
+
+		@Test
+		public void entryUntouchedOnPrefixFailure() throws IOException {
+			Mockito.when(clearPath.startsWith(oldPrefixPath)).thenReturn(false);
+
+			cache.get(clearPath, dirLoader); //triggers loader
+			cache.recomputeAllKeysWithPrefix(oldPrefixPath, newPrefixPath);
+			cache.get(clearPath, dirLoader); //does not trigger
+			cache.get(newClearPath, dirLoader); //does trigger
+
+			Mockito.verify(dirLoader, Mockito.times(2)).load();
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 5095b5cd..3b7d8462 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -1,6 +1,5 @@
 package org.cryptomator.cryptofs;
 
-import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
 import org.cryptomator.cryptofs.attr.AttributeByNameProvider;
 import org.cryptomator.cryptofs.attr.AttributeProvider;
 import org.cryptomator.cryptofs.attr.AttributeViewProvider;
@@ -1258,11 +1257,11 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException
 			Path ciphertextDirPath = mock(Path.class, "d/FF/FF/");
 			CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext");
 			String dirId = "DirId1234ABC";
-			CiphertextDirectory cipherDirObject = new CiphertextDirectory(dirId, ciphertextDirPath);
+			CiphertextDirectory ciphertextDirectoryObject = new CiphertextDirectory(dirId, ciphertextDirPath);
 			FileChannelMock channel = new FileChannelMock(100);
 			when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
 			when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
-			when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(cipherDirObject);
+			when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(ciphertextDirectoryObject);
 			when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextParent));
 			when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class);
 			when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath);
@@ -1278,7 +1277,7 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException
 			inTest.createDirectory(path);
 
 			verify(readonlyFlag).assertWritable();
-			verify(dirIdBackup, Mockito.times(1)).execute(cipherDirObject);
+			verify(dirIdBackup, Mockito.times(1)).execute(ciphertextDirectoryObject);
 		}
 
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
index 32241fb8..93cdda8d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
@@ -64,6 +64,7 @@ public void setup() {
 		Mockito.when(fileSystem.getEmptyPath()).thenReturn(empty);
 	}
 
+
 	@Test
 	public void testPathEncryptionForRoot() throws IOException {
 		Path d00 = Mockito.mock(Path.class);
@@ -74,7 +75,7 @@ public void testPathEncryptionForRoot() throws IOException {
 		Mockito.when(d00.resolve("00")).thenReturn(d0000);
 
 		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
-		Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path;
+		Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path();
 		Assertions.assertEquals(d0000, path);
 	}
 
@@ -98,7 +99,7 @@ public void testPathEncryptionForFoo() throws IOException {
 		Mockito.when(d00.resolve("01")).thenReturn(d0001);
 
 		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
-		Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path;
+		Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path();
 		Assertions.assertEquals(d0001, path);
 	}
 
@@ -132,7 +133,7 @@ public void testPathEncryptionForFooBar() throws IOException {
 		Mockito.when(d00.resolve("02")).thenReturn(d0002);
 
 		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
-		Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path;
+		Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path();
 		Assertions.assertEquals(d0002, path);
 	}
 
@@ -333,7 +334,113 @@ public void testGetCiphertextFileTypeForShortenedFile() throws IOException {
 			Assertions.assertEquals(CiphertextFileType.FILE, type);
 		}
 
+	}
+
+	@Nested
+	public class InvalidateOrMovePathMapping {
+
+		Path d00 = Mockito.mock(Path.class);
+		Path d0000 = Mockito.mock(Path.class, "d/00/00");
+		Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r");
+		Path d0000oofdirFile = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r");
+		Path d0001 = Mockito.mock(Path.class, "d/00/01");
+		Path d0001rab = Mockito.mock(Path.class, "d/00/01/rab.c9r");
+		Path d0000rabdirFile = Mockito.mock(Path.class, "d/00/00/rab.c9r/dir.c9r");
+		Path d0002 = Mockito.mock(Path.class);
+		Path d0000kik = Mockito.mock(Path.class, "d/00/00/kik.c9r");
+		Path d0000kikdirFile = Mockito.mock(Path.class, "d/00/00/kik.c9r/dir.c9r");
+		Path d0003 = Mockito.mock(Path.class, "d/00/03/kik.c9r");
+
+		@BeforeEach
+		void beforeEach() throws IOException {
+			Mockito.when(dataRoot.resolve("00")).thenReturn(d00);
+			Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000");
+
+			// /foo
+			Mockito.when(d00.resolve("00")).thenReturn(d0000);
+			Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof);
+			Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdirFile);
+			Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("oof");
+			Mockito.when(dirIdProvider.load(d0000oofdirFile)).thenReturn("1");
+			Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001");
+
+			// /kik
+			Mockito.when(d0000.resolve("kik.c9r")).thenReturn(d0000kik);
+			Mockito.when(d0000kik.resolve("dir.c9r")).thenReturn(d0000kikdirFile);
+			Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("kik"), Mockito.any())).thenReturn("kik");
+			Mockito.when(dirIdProvider.load(d0000kikdirFile)).thenReturn("3");
+			Mockito.when(fileNameCryptor.hashDirectoryId("3")).thenReturn("0003");
+
+			// /foo/bar
+			Mockito.when(d00.resolve("01")).thenReturn(d0001);
+			Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab);
+			Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdirFile);
+			Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("bar"), Mockito.any())).thenReturn("rab");
+			Mockito.when(dirIdProvider.load(d0000rabdirFile)).thenReturn("2");
+			Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002");
+
+			Mockito.when(d00.resolve("02")).thenReturn(d0002);
+			Mockito.when(d00.resolve("03")).thenReturn(d0003);
+		}
+
+		@Test
+		@DisplayName("Invalidating node causes cache miss on next retrieval")
+		public void testRemovedEntryMiss() throws IOException {
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
+			var fooPath = fileSystem.getPath("/foo");
+			mapper.getCiphertextDir(fooPath);
+			mapper.invalidatePathMapping(fooPath);
+			var mapperSpy = Mockito.spy(mapper);
+			mapperSpy.getCiphertextDir(fooPath);
+			Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooPath); //loader is triggered, hence we have a cache miss
+		}
+
+		@Test
+		@DisplayName("Invalidating node also invalidates all children")
+		public void testRemovedEntryChildMiss() throws IOException {
+			var fooPath = fileSystem.getPath("/foo");
+			var fooBarPath = fileSystem.getPath("/foo/bar");
+
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
+			mapper.getCiphertextDir(fooPath);
+			mapper.getCiphertextDir(fooBarPath);
+			mapper.invalidatePathMapping(fooPath);
+			var mapperSpy = Mockito.spy(mapper);
+			mapperSpy.getCiphertextDir(fooBarPath);
+			Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooBarPath); //loader is triggered, hence we have a cache miss
+			mapperSpy.getCiphertextDir(fooBarPath);
+		}
+
+		@Test
+		@DisplayName("Moving node causes cache miss for oldPath and cache hit for new")
+		public void testMoveEntryOldMissNewHit() throws IOException {
+			var fooPath = fileSystem.getPath("/foo");
+			var kikPath = fileSystem.getPath("/kik");
+
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
+			mapper.getCiphertextDir(fooPath);
+			mapper.movePathMapping(fooPath, kikPath);
+			var mapperSpy = Mockito.spy(mapper);
+			mapperSpy.getCiphertextDir(fooPath);
+			Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooPath); //loader is triggered, hence we have a cache miss
+			Mockito.verify(mapperSpy, Mockito.never()).getCiphertextFilePath(kikPath); //loader is not triggered, hence we have a cache hit
+		}
 
+		@Test
+		@DisplayName("Moving node causes cache miss for childs of oldPath")
+		public void testMoveEntryOldChildMiss() throws IOException {
+			var fooPath = fileSystem.getPath("/foo");
+			var fooBarPath = fileSystem.getPath("/foo/bar");
+			var kikPath = fileSystem.getPath("/kik");
+
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
+			mapper.getCiphertextDir(fooPath);
+			mapper.getCiphertextDir(fooBarPath);
+			mapper.movePathMapping(fooPath, kikPath);
+			var mapperSpy = Mockito.spy(mapper);
+			mapperSpy.getCiphertextDir(fooBarPath);
+			Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooBarPath); //loader is triggered, hence we have a cache miss
+		}
 	}
 
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
index 19805b25..58739e90 100644
--- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
@@ -22,7 +22,7 @@ public class DirectoryIdBackupTest {
 	Path contentPath;
 
 	private String dirId = "12345678";
-	private CryptoPathMapper.CiphertextDirectory cipherDirObject;
+	private CiphertextDirectory ciphertextDirectoryObject;
 	private EncryptingWritableByteChannel encChannel;
 	private Cryptor cryptor;
 
@@ -31,7 +31,7 @@ public class DirectoryIdBackupTest {
 
 	@BeforeEach
 	public void init() {
-		cipherDirObject = new CryptoPathMapper.CiphertextDirectory(dirId, contentPath);
+		ciphertextDirectoryObject = new CiphertextDirectory(dirId, contentPath);
 		cryptor = Mockito.mock(Cryptor.class);
 		encChannel = Mockito.mock(EncryptingWritableByteChannel.class);
 
@@ -44,7 +44,7 @@ public void testIdFileCreated() throws IOException {
 			backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel);
 			Mockito.when(encChannel.write(Mockito.any())).thenReturn(0);
 
-			dirIdBackup.execute(cipherDirObject);
+			dirIdBackup.execute(ciphertextDirectoryObject);
 
 			Assertions.assertTrue(Files.exists(contentPath.resolve(Constants.DIR_BACKUP_FILE_NAME)));
 		}
@@ -58,7 +58,7 @@ public void testContentIsWritten() throws IOException {
 		try (MockedStatic<DirectoryIdBackup> backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) {
 			backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel);
 
-			dirIdBackup.execute(cipherDirObject);
+			dirIdBackup.execute(ciphertextDirectoryObject);
 
 			Mockito.verify(encChannel, Mockito.times(1)).write(Mockito.argThat(b -> b.equals(expectedWrittenContent)));
 		}
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java
index d123f3df..33230151 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java
@@ -8,10 +8,10 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.attr;
 
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.CiphertextFilePath;
 import org.cryptomator.cryptofs.CryptoPath;
 import org.cryptomator.cryptofs.CryptoPathMapper;
-import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
 import org.cryptomator.cryptofs.Symlinks;
 import org.cryptomator.cryptofs.common.CiphertextFileType;
 import org.junit.jupiter.api.Assertions;
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java
index e18810e1..c2bf66a9 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java
@@ -1,5 +1,6 @@
 package org.cryptomator.cryptofs.dir;
 
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.CryptoPathMapper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -31,7 +32,7 @@ public void testProcessNormalDirectoryNode(@TempDir Path dir) throws IOException
 		Path targetDir = Files.createDirectories(dir.resolve("d/ab/cdefg"));
 		Files.createDirectory(dir.resolve("foo.c9r"));
 		Files.write(dir.resolve("foo.c9r/dir.c9r"), "".getBytes());
-		Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CryptoPathMapper.CiphertextDirectory("asd", targetDir));
+		Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CiphertextDirectory("asd", targetDir));
 		Node unfiltered = new Node(dir.resolve("foo.c9r"));
 
 		Stream<Node> result = brokenDirectoryFilter.process(unfiltered);
@@ -45,7 +46,7 @@ public void testProcessNodeWithMissingTargetDir(@TempDir Path dir) throws IOExce
 		Path targetDir = dir.resolve("d/ab/cdefg"); // not existing!
 		Files.createDirectory(dir.resolve("foo.c9r"));
 		Files.write(dir.resolve("foo.c9r/dir.c9r"), "".getBytes());
-		Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CryptoPathMapper.CiphertextDirectory("asd", targetDir));
+		Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CiphertextDirectory("asd", targetDir));
 		Node unfiltered = new Node(dir.resolve("foo.c9r"));
 
 		Stream<Node> result = brokenDirectoryFilter.process(unfiltered);
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java
index c4361278..eada83a8 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java
@@ -1,8 +1,8 @@
 package org.cryptomator.cryptofs.dir;
 
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.CryptoPath;
 import org.cryptomator.cryptofs.CryptoPathMapper;
-import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java
index 8a9aef15..10e9ba35 100644
--- a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java
@@ -1,6 +1,6 @@
 package org.cryptomator.cryptofs.health.dirid;
 
-import org.cryptomator.cryptofs.CryptoPathMapper;
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.DirectoryIdBackup;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptolib.api.Cryptor;
@@ -60,7 +60,7 @@ public void testFix() throws IOException {
 			result.fix(pathToVault, cryptor);
 
 			var expectedPath = pathToVault.resolve("d/ri/diculous-32-char-pseudo-hashhh");
-			ArgumentMatcher<CryptoPathMapper.CiphertextDirectory> cipherDirMatcher = obj -> obj.dirId.equals(dirId) && obj.path.endsWith(expectedPath);
+			ArgumentMatcher<CiphertextDirectory> cipherDirMatcher = obj -> obj.dirId().equals(dirId) && obj.path().endsWith(expectedPath);
 			dirIdBackupMock.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1));
 			var attr = Assertions.assertDoesNotThrow(() -> Files.readAttributes(expectedPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS));
 			Assertions.assertTrue(attr.isDirectory());
diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java
index 09aedef5..cb97d0b7 100644
--- a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java
@@ -1,6 +1,6 @@
 package org.cryptomator.cryptofs.health.dirid;
 
-import org.cryptomator.cryptofs.CryptoPathMapper;
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.DirectoryIdBackup;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptolib.api.Cryptor;
@@ -43,7 +43,7 @@ public void testFix() throws IOException {
 			result.fix(pathToVault, cryptor);
 
 			var expectedPath = pathToVault.resolve(cipherDir);
-			ArgumentMatcher<CryptoPathMapper.CiphertextDirectory> cipherDirMatcher = obj -> obj.dirId.equals(dirId) && obj.path.isAbsolute() && obj.path.equals(expectedPath);
+			ArgumentMatcher<CiphertextDirectory> cipherDirMatcher = obj -> obj.dirId().equals(dirId) && obj.path().isAbsolute() && obj.path().equals(expectedPath);
 			dirIdBackupMock.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1));
 		}
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java
index fcc5888e..9f9c93b2 100644
--- a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java
@@ -1,7 +1,7 @@
 package org.cryptomator.cryptofs.health.dirid;
 
 import com.google.common.io.BaseEncoding;
-import org.cryptomator.cryptofs.CryptoPathMapper;
+import org.cryptomator.cryptofs.CiphertextDirectory;
 import org.cryptomator.cryptofs.DirectoryIdBackup;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.common.Constants;
@@ -20,7 +20,6 @@
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
@@ -327,15 +326,15 @@ public void testAdoptOrphanedUnshortened() throws IOException {
 			String newClearName = "OliverTwist";
 			Files.writeString(oldCipherPath, expectedMsg, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
 
-			CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222"));
-			Files.createDirectories(stepParentDir.path);
+			CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222"));
+			Files.createDirectories(stepParentDir.path());
 
-			Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(BaseEncoding.base64Url(), newClearName, stepParentDir.dirId.getBytes(StandardCharsets.UTF_8));
+			Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(BaseEncoding.base64Url(), newClearName, stepParentDir.dirId().getBytes(StandardCharsets.UTF_8));
 			var sha1 = Mockito.mock(MessageDigest.class);
 
 			result.adoptOrphanedResource(oldCipherPath, newClearName, false, stepParentDir, fileNameCryptor, sha1);
 
-			Assertions.assertEquals(expectedMsg, Files.readString(stepParentDir.path.resolve("adopted.c9r")));
+			Assertions.assertEquals(expectedMsg, Files.readString(stepParentDir.path().resolve("adopted.c9r")));
 			Assertions.assertTrue(Files.notExists(oldCipherPath));
 		}
 
@@ -348,8 +347,8 @@ public void testAdoptOrphanedShortened() throws IOException {
 			Files.createDirectories(oldCipherPath);
 			Files.createFile(oldCipherPath.resolve("name.c9s"));
 
-			CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222"));
-			Files.createDirectories(stepParentDir.path);
+			CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222"));
+			Files.createDirectories(stepParentDir.path());
 
 			Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(Mockito.any(), Mockito.any(), Mockito.any());
 			try (var baseEncodingClass = Mockito.mockStatic(BaseEncoding.class)) {
@@ -363,8 +362,8 @@ public void testAdoptOrphanedShortened() throws IOException {
 				result.adoptOrphanedResource(oldCipherPath, newClearName, true, stepParentDir, fileNameCryptor, sha1);
 			}
 
-			Assertions.assertTrue(Files.exists(stepParentDir.path.resolve("adopted_shortened.c9s")));
-			Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path.resolve("adopted_shortened.c9s/name.c9s")));
+			Assertions.assertTrue(Files.exists(stepParentDir.path().resolve("adopted_shortened.c9s")));
+			Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path().resolve("adopted_shortened.c9s/name.c9s")));
 			Assertions.assertTrue(Files.notExists(oldCipherPath));
 		}
 
@@ -376,8 +375,8 @@ public void testAdoptOrphanedShortenedMissingNameC9s() throws IOException {
 			String newClearName = "TomSawyer";
 			Files.createDirectories(oldCipherPath);
 
-			CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222"));
-			Files.createDirectories(stepParentDir.path);
+			CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222"));
+			Files.createDirectories(stepParentDir.path());
 
 			Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(Mockito.any(), Mockito.any(), Mockito.any());
 			try (var baseEncodingClass = Mockito.mockStatic(BaseEncoding.class)) {
@@ -391,8 +390,8 @@ public void testAdoptOrphanedShortenedMissingNameC9s() throws IOException {
 				result.adoptOrphanedResource(oldCipherPath, newClearName, true, stepParentDir, fileNameCryptor, sha1);
 			}
 
-			Assertions.assertTrue(Files.exists(stepParentDir.path.resolve("adopted_shortened.c9s")));
-			Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path.resolve("adopted_shortened.c9s/name.c9s")));
+			Assertions.assertTrue(Files.exists(stepParentDir.path().resolve("adopted_shortened.c9s")));
+			Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path().resolve("adopted_shortened.c9s/name.c9s")));
 			Assertions.assertTrue(Files.notExists(oldCipherPath));
 		}
 
@@ -410,7 +409,7 @@ public void testFixNoDirId() throws IOException {
 		Files.createFile(orphan1);
 		Files.createDirectories(orphan2);
 
-		CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
+		CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
 
 		VaultConfig config = Mockito.mock(VaultConfig.class);
 		Mockito.doReturn(170).when(config).getShorteningThreshold();
@@ -443,7 +442,7 @@ public void testFixContinuesOnNotRecoverableFilename() throws IOException {
 
 		var dirId = Optional.of("trololo-id");
 
-		CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
+		CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
 
 		VaultConfig config = Mockito.mock(VaultConfig.class);
 		Mockito.doReturn(170).when(config).getShorteningThreshold();
@@ -482,7 +481,7 @@ public void testFixWithDirId() throws IOException {
 
 		var dirId = Optional.of("trololo-id");
 
-		CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
+		CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
 
 		VaultConfig config = Mockito.mock(VaultConfig.class);
 		Mockito.doReturn(170).when(config).getShorteningThreshold();
@@ -525,8 +524,8 @@ public void testFixWithNonCryptomatorFiles() throws IOException {
 
 		var dirId = Optional.of("trololo-id");
 
-		CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
-		Files.createDirectories(stepParentDir.path); //needs to be created here, otherwise the Files.move(non-crypto-resource, stepparent) will fail
+		CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222"));
+		Files.createDirectories(stepParentDir.path()); //needs to be created here, otherwise the Files.move(non-crypto-resource, stepparent) will fail
 
 		VaultConfig config = Mockito.mock(VaultConfig.class);
 		Mockito.doReturn(170).when(config).getShorteningThreshold();
@@ -548,7 +547,7 @@ public void testFixWithNonCryptomatorFiles() throws IOException {
 		Mockito.verify(resultSpy, Mockito.never()).adoptOrphanedResource(Mockito.eq(unrelated), Mockito.any(), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any());
 		Mockito.verify(resultSpy, Mockito.times(1)).adoptOrphanedResource(Mockito.eq(orphan1), Mockito.eq(lostName1), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any());
 		Mockito.verify(resultSpy, Mockito.times(1)).adoptOrphanedResource(Mockito.eq(orphan2), Mockito.eq(lostName2), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any());
-		Assertions.assertTrue(Files.exists(stepParentDir.path.resolve("unrelated.file")));
+		Assertions.assertTrue(Files.exists(stepParentDir.path().resolve("unrelated.file")));
 		Assertions.assertTrue(Files.notExists(cipherOrphan));
 	}