en | fr

Reactor Expand vs ExpandDeep: Directory Traversal Strategies

Published on 2024-04-10 | Updated on 2025-12-02 | 8 mins read | Tutorial Reactive Programming

When exploring directory structures in a reactive programming environment, the strategies of breadth-first and depth-first traversal play a crucial role. This guide explores how to use Reactor’s expand and expandDeep operators to traverse and emit paths in a reactive stream.

1. Overview

Project Reactor provides two powerful operators for recursive exploration: expand (breadth-first) and expandDeep (depth-first). These operators are particularly useful when working with hierarchical structures like file systems, where you need to traverse directories and process files reactively.

The ReactorFileUtils class demonstrates practical implementations of both strategies for exploring directory structures in a non-blocking, reactive manner.

2. Traversal Strategies

2.1. Breadth-First Traversal

Breadth-first traversal explores a tree structure level by level, processing all nodes at the current depth before moving to the next level.

Diagram

2.1.1. Characteristics

  • Starting Point: Begins with the root directory path

  • Exploration: Checks if the path is a directory, lists immediate child paths, and emits them level by level

  • Processing Order: Processes all immediate neighbors before moving deeper

  • Use Case: Useful when you need to process directories closest to the root first

2.1.2. Example

For a directory structure A → B, C → D, E, the exploration order is:

  1. A (root)

  2. B, C (level 1)

  3. D, E (level 2)

2.2. Depth-First Traversal

Depth-first traversal explores a tree structure by going as deep as possible along each branch before backtracking.

Diagram

2.2.1. Characteristics

  • Starting Point: Starts with the root directory path

  • Exploration: Checks if the path is a directory, lists immediate child paths, and recursively explores each branch completely

  • Processing Order: Goes as deep as possible before backtracking

  • Use Case: Useful for fully exploring specific branches before moving on

2.2.2. Example

For a directory structure A → B, C → D, E, the exploration order is:

  1. A (root)

  2. B (first branch)

  3. D, E (explore B’s children completely)

  4. C (backtrack and move to second branch)

3. Project Setup

3.2. Maven Configuration

pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.maoudia.lib</groupId>
    <artifactId>app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>maoudia-lib</name>
    <description>MAOUDIA LIB</description>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>3.6.2</version>
        </dependency>

        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <version>3.6.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

4. Implementation

4.1. ReactorFileUtils Class

This utility class provides reactive file operations using Project Reactor.

ReactorFileUtils.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.maoudia;

import jakarta.validation.constraints.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.NonNull;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;

public class ReactorFileUtils {

    @NotNull
    public static Flux<Path> expand(@NonNull Path path) { (1)
        return Mono.justOrEmpty(path)
                .filter(Files::isDirectory)
                .expand(ReactorFileUtils::listPaths);
    }

    @NotNull
    public static Flux<Path> expandDeep(@NonNull Path path) { (2)
        return Mono.justOrEmpty(path)
                .filter(Files::isDirectory)
                .expandDeep(ReactorFileUtils::listPaths);
    }

    @NotNull
    public static Flux<Path> listPaths(@NonNull Path path) { (3)
        return Mono.justOrEmpty(path)
                .filter(Files::isDirectory)
                .mapNotNull(directory -> directory.toFile().listFiles())
                .flatMapMany(Flux::fromArray)
                .map(File::toPath);
    }
}
1Breadth-first traversal: explores directories level by level using expand operator.
2Depth-first traversal: explores directories deeply along each branch using expandDeep operator.
3Lists all paths within a directory and emits them as a reactive stream.

4.2. Understanding the Implementation

4.2.1. expand Method

The expand method implements breadth-first traversal:

return Mono.justOrEmpty(path)
    .filter(Files::isDirectory)
    .expand(ReactorFileUtils::listPaths);
  • Creates a Mono from the input path

  • Filters to ensure it’s a directory

  • Uses expand to recursively list paths, processing each level completely before moving deeper

4.2.2. expandDeep Method

The expandDeep method implements depth-first traversal:

return Mono.justOrEmpty(path)
    .filter(Files::isDirectory)
    .expandDeep(ReactorFileUtils::listPaths);
  • Creates a Mono from the input path

  • Filters to ensure it’s a directory

  • Uses expandDeep to recursively explore each branch fully before backtracking

4.2.3. listPaths Method

The listPaths method provides the expansion logic:

return Mono.justOrEmpty(path)
    .filter(Files::isDirectory)
    .mapNotNull(directory -> directory.toFile().listFiles())
    .flatMapMany(Flux::fromArray)
    .map(File::toPath);
  • Creates a Mono from the path

  • Filters directories only

  • Lists all files in the directory

  • Converts the array to a Flux

  • Maps each File to a Path

5. Testing

5.1. Test Structure

The tests use StepVerifier from reactor-test to verify the traversal order:

ReactorFileUtilsTest.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.maoudia;

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

import java.nio.file.Path;
import java.nio.file.Paths;

class ReactorFileUtilsTest {

    private static final Path ROOT_DIRECTORY = Paths.get("src/test/resources");

    @Test
    void expand_ValidPath_ReturnsExpectedPaths() { (1)
        Flux<Path> pathFlux = ReactorFileUtils.expand(ROOT_DIRECTORY);

        StepVerifier.create(pathFlux)
                .expectNextMatches(path -> path.endsWith("resources"))
                .expectNextMatches(path -> path.endsWith("file2.txt"))
                .expectNextMatches(path -> path.endsWith("file1.txt"))
                .expectNextMatches(path -> path.endsWith("subdirectory"))
                .expectNextMatches(path -> path.endsWith("emptydirectory"))
                .expectNextMatches(path -> path.endsWith("subfile1.txt"))
                .expectNextMatches(path -> path.endsWith("subfile2.txt"))
                .expectNextMatches(path -> path.endsWith("subsubdirectory"))
                .expectNextMatches(path -> path.endsWith("subsubfile1.txt"))
                .expectNextMatches(path -> path.endsWith("subsubfile2.txt"))
                .verifyComplete();
    }

    @Test
    void expand_NullPath_ReturnsEmptyFlux() { (2)
        Flux<Path> pathFlux = ReactorFileUtils.expand(null);

        StepVerifier.create(pathFlux)
                .verifyComplete();
    }

    @Test
    void expandDeep_ValidPath_ReturnsExpectedPaths() { (3)
        Flux<Path> pathFlux = ReactorFileUtils.expandDeep(ROOT_DIRECTORY);

        StepVerifier.create(pathFlux)
                .expectNextMatches(path -> path.endsWith("resources"))
                .expectNextMatches(path -> path.endsWith("file2.txt"))
                .expectNextMatches(path -> path.endsWith("file1.txt"))
                .expectNextMatches(path -> path.endsWith("subdirectory"))
                .expectNextMatches(path -> path.endsWith("subfile1.txt"))
                .expectNextMatches(path -> path.endsWith("subfile2.txt"))
                .expectNextMatches(path -> path.endsWith("subsubdirectory"))
                .expectNextMatches(path -> path.endsWith("subsubfile1.txt"))
                .expectNextMatches(path -> path.endsWith("subsubfile2.txt"))
                .expectNextMatches(path -> path.endsWith("emptydirectory"))
                .verifyComplete();
    }

    @Test
    void expandDeep_NullPath_ReturnsEmptyFlux() { (4)
        Flux<Path> pathFlux = ReactorFileUtils.expand(null);

        StepVerifier.create(pathFlux)
                .verifyComplete();
    }

    @Test
    void listFiles_ValidDirectory_ReturnsExpectedPaths() { (5)
        Flux<Path> pathFlux = ReactorFileUtils.listPaths(ROOT_DIRECTORY);

        StepVerifier.create(pathFlux)
                .expectNextMatches(path -> path.endsWith("file2.txt"))
                .expectNextMatches(path -> path.endsWith("file1.txt"))
                .expectNextMatches(path -> path.endsWith("subdirectory"))
                .expectNextMatches(path -> path.endsWith("emptydirectory"))
                .verifyComplete();
    }

    @Test
    void listFiles_NullPath_ReturnsEmptyFlux() { (6)
        Flux<Path> pathFlux = ReactorFileUtils.listPaths(null);

        StepVerifier.create(pathFlux)
                .verifyComplete();
    }
}
1Tests breadth-first traversal, verifying that paths are emitted level by level.
2Tests null path handling for expand method, expecting an empty flux.
3Tests depth-first traversal, verifying that paths are emitted branch by branch.
4Tests null path handling for expandDeep method, expecting an empty flux.
5Tests listing immediate children of a directory without recursion.
6Tests null path handling for listPaths method, expecting an empty flux.

5.2. Test Directory Structure

The tests assume the following directory structure in src/test/resources:

resources/
├── file1.txt
├── file2.txt
├── emptydirectory/
└── subdirectory/
    ├── subfile1.txt
    ├── subfile2.txt
    └── subsubdirectory/
        ├── subsubfile1.txt
        └── subsubfile2.txt

5.3. Observing Traversal Differences

Notice the difference in traversal order between the two tests:

Breadth-First (expand):

resources → file2.txt → file1.txt → subdirectory → emptydirectory
         → subfile1.txt → subfile2.txt → subsubdirectory
         → subsubfile1.txt → subsubfile2.txt

Depth-First (expandDeep):

resources → file2.txt → file1.txt → subdirectory
         → subfile1.txt → subfile2.txt → subsubdirectory
         → subsubfile1.txt → subsubfile2.txt → emptydirectory

The key difference: emptydirectory appears earlier in breadth-first (same level as subdirectory) but later in depth-first (after fully exploring subdirectory).

6. Use Cases and Recommendations

6.1. When to Use Breadth-First (expand)

  • Finding files at specific depths: When you need to process all files at a certain level before going deeper

  • Memory efficiency: When exploring very deep hierarchies where depth-first might cause stack issues

  • Immediate neighbor processing: When you need to process files closest to the root first

  • Load balancing: When distributing work across multiple levels

6.2. When to Use Depth-First (expandDeep)

  • Complete branch exploration: When you need to fully process one directory tree before moving to siblings

  • Resource cleanup: When processing requires completing an entire branch before starting another

  • Path-dependent operations: When operations depend on completing parent-child relationships

  • Search optimization: When looking for specific files and want to explore paths completely

7. Best Practices

7.1. Error Handling

Add proper error handling to the reactive chain:

ReactorFileUtils.expand(path)
    .onErrorResume(IOException.class, e -> {
        log.error("Failed to traverse directory", e);
        return Flux.empty();
    })
    .subscribe();

7.2. Backpressure Management

For large directory structures, consider using backpressure operators:

ReactorFileUtils.expand(path)
    .limitRate(100)
    .onBackpressureBuffer(1000)
    .subscribe();

7.3. Resource Management

Ensure proper resource cleanup when working with file streams:

ReactorFileUtils.expand(path)
    .doOnCancel(() -> log.info("Traversal cancelled"))
    .doFinally(signalType -> log.info("Traversal completed: {}", signalType))
    .subscribe();

7.4. Filtering and Transformation

Combine traversal with filtering and transformation:

ReactorFileUtils.expand(path)
    .filter(p -> p.toString().endsWith(".txt"))
    .map(Path::getFileName)
    .subscribe(System.out::println);

8. Performance Considerations

8.1. Memory Usage

  • Breadth-first: Uses more memory as it needs to keep track of all nodes at the current level

  • Depth-first: Uses less memory but may have deeper recursion stacks

8.2. Processing Speed

  • Breadth-first: Better for parallel processing of same-level items

  • Depth-first: Better for sequential processing of complete branches

8.3. Cancellation

Both strategies support cancellation through Reactor’s subscription mechanism:

Disposable subscription = ReactorFileUtils.expand(path)
    .subscribe(System.out::println);

subscription.dispose();

9. Comparison Summary

AspectBreadth-First (expand)Depth-First (expandDeep)

Traversal Order

Level by level

Branch by branch

Processing

Immediate neighbors first

Complete branch exploration

Memory Usage

Higher (stores level nodes)

Lower (recursive depth)

Use Case

Level-based operations

Branch-based operations

Best For

Finding files at specific depths

Complete directory processing

10. Conclusion

Understanding the difference between breadth-first and depth-first traversal is crucial for efficient reactive directory exploration. Project Reactor’s expand and expandDeep operators provide powerful tools for implementing these strategies in a non-blocking, reactive manner.

Key takeaways:

  • Use expand (breadth-first) when you need to process directories level by level

  • Use expandDeep (depth-first) when you need to fully explore branches before moving on

  • Both strategies integrate seamlessly with Reactor’s reactive programming model

  • Choose the appropriate strategy based on your specific use case and requirements

The complete source code is available on GitHub Gist.