diff --git a/src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java b/src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java
new file mode 100644
index 000000000000..2cf4ed23c44f
--- /dev/null
+++ b/src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java
@@ -0,0 +1,163 @@
+package com.thealgorithms.graph;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * DFS that visits a successor only when all its predecessors are already visited,
+ * emitting VISIT and SKIP events.
+ *
+ * This class includes a DFS variant that visits a successor only when all of its
+ * predecessors have already been visited
+ *
+ * Related reading:
+ *
+ *
+ */
+
+public final class PredecessorConstrainedDfs {
+
+ private PredecessorConstrainedDfs() {
+ // utility class
+ }
+
+ /** An event emitted by the traversal: either a VISIT with an order, or a SKIP with a note. */
+ public record TraversalEvent(T node,
+ Integer order, // non-null for visit, null for skip
+ String note // non-null for skip, null for visit
+ ) {
+ public TraversalEvent {
+ Objects.requireNonNull(node);
+ // order and note can be null based on event type
+ }
+
+ /** A visit event with an increasing order (0,1,2,...) */
+ public static TraversalEvent visit(T node, int order) {
+ return new TraversalEvent<>(node, order, null);
+ }
+
+ /** A skip event with an explanatory note (e.g., not all parents visited yet). */
+ public static TraversalEvent skip(T node, String note) {
+ return new TraversalEvent<>(node, null, Objects.requireNonNull(note));
+ }
+
+ public boolean isVisit() {
+ return order != null;
+ }
+
+ public boolean isSkip() {
+ return order == null;
+ }
+
+ @Override
+ public String toString() {
+ return isVisit() ? "VISIT(" + node + ", order=" + order + ")" : "SKIP(" + node + ", " + note + ")";
+ }
+ }
+
+ /**
+ * DFS (recursive) that records the order of first visit starting at {@code start},
+ * but only recurses to a child when all its predecessors have been visited.
+ * If a child is encountered early (some parent unvisited), a SKIP event is recorded.
+ *
+ * Equivalent idea to the Python pseudo in the user's description (with successors and predecessors),
+ * but implemented in Java and returning a sequence of {@link TraversalEvent}s.
+ *
+ * @param successors adjacency list: for each node, its outgoing neighbors
+ * @param start start node
+ * @return immutable list of traversal events (VISITs with monotonically increasing order and SKIPs with messages)
+ * @throws IllegalArgumentException if {@code successors} is null
+ */
+ public static List> dfsRecursiveOrder(Map> successors, T start) {
+ if (successors == null) {
+ throw new IllegalArgumentException("successors must not be null");
+ }
+ // derive predecessors once
+ Map> predecessors = derivePredecessors(successors);
+ return dfsRecursiveOrder(successors, predecessors, start);
+ }
+
+ /**
+ * Same as {@link #dfsRecursiveOrder(Map, Object)} but with an explicit predecessors map.
+ */
+ public static List> dfsRecursiveOrder(Map> successors, Map> predecessors, T start) {
+
+ if (successors == null || predecessors == null) {
+ throw new IllegalArgumentException("successors and predecessors must not be null");
+ }
+ if (start == null) {
+ return List.of();
+ }
+ if (!successors.containsKey(start) && !appearsAnywhere(successors, start)) {
+ return List.of(); // start not present in graph
+ }
+
+ List> events = new ArrayList<>();
+ Set visited = new HashSet<>();
+ int[] order = {0};
+ dfs(start, successors, predecessors, visited, order, events);
+ return Collections.unmodifiableList(events);
+ }
+
+ private static void dfs(T currentNode, Map> successors, Map> predecessors, Set visited, int[] order, List> result) {
+
+ if (!visited.add(currentNode)) {
+ return; // already visited
+ }
+ result.add(TraversalEvent.visit(currentNode, order[0]++)); // record visit and increment
+
+ for (T childNode : successors.getOrDefault(currentNode, List.of())) {
+ if (visited.contains(childNode)) {
+ continue;
+ }
+ if (allParentsVisited(childNode, visited, predecessors)) {
+ dfs(childNode, successors, predecessors, visited, order, result);
+ } else {
+ result.add(TraversalEvent.skip(childNode, "⛔ Skipping " + childNode + ": not all parents are visited yet."));
+ // do not mark visited; it may be visited later from another parent
+ }
+ }
+ }
+
+ private static boolean allParentsVisited(T node, Set visited, Map> predecessors) {
+ for (T parent : predecessors.getOrDefault(node, List.of())) {
+ if (!visited.contains(parent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean appearsAnywhere(Map> successors, T node) {
+ if (successors.containsKey(node)) {
+ return true;
+ }
+ for (List neighbours : successors.values()) {
+ if (neighbours != null && neighbours.contains(node)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Map> derivePredecessors(Map> successors) {
+ Map> predecessors = new HashMap<>();
+ // ensure keys exist for all nodes appearing anywhere
+ for (Map.Entry> entry : successors.entrySet()) {
+ predecessors.computeIfAbsent(entry.getKey(), key -> new ArrayList<>());
+ for (T childNode : entry.getValue()) {
+ predecessors.computeIfAbsent(childNode, key -> new ArrayList<>()).add(entry.getKey());
+ }
+ }
+ return predecessors;
+ }
+}
diff --git a/src/test/java/com/thealgorithms/graph/PredecessorConstrainedDfsTest.java b/src/test/java/com/thealgorithms/graph/PredecessorConstrainedDfsTest.java
new file mode 100644
index 000000000000..e2c6d468768f
--- /dev/null
+++ b/src/test/java/com/thealgorithms/graph/PredecessorConstrainedDfsTest.java
@@ -0,0 +1,90 @@
+package com.thealgorithms.graph;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.thealgorithms.graph.PredecessorConstrainedDfs.TraversalEvent;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class PredecessorConstrainedDfsTest {
+
+ // A -> B, A -> C, B -> D, C -> D (classic diamond)
+ private static Map> diamond() {
+ Map> g = new LinkedHashMap<>();
+ g.put("A", List.of("B", "C"));
+ g.put("B", List.of("D"));
+ g.put("C", List.of("D"));
+ g.put("D", List.of());
+ return g;
+ }
+
+ @Test
+ void dfsRecursiveOrderEmitsSkipUntilAllParentsVisited() {
+ List> events = PredecessorConstrainedDfs.dfsRecursiveOrder(diamond(), "A");
+
+ // Expect visits in order and a skip for first time we meet D (via B) before C is visited.
+ var visits = events.stream().filter(TraversalEvent::isVisit).toList();
+ var skips = events.stream().filter(TraversalEvent::isSkip).toList();
+
+ // Visits should be A(0), B(1), C(2), D(3) in some deterministic order given adjacency
+ assertThat(visits).hasSize(4);
+ assertThat(visits.get(0).node()).isEqualTo("A");
+ assertThat(visits.get(0).order()).isEqualTo(0);
+ assertThat(visits.get(1).node()).isEqualTo("B");
+ assertThat(visits.get(1).order()).isEqualTo(1);
+ assertThat(visits.get(2).node()).isEqualTo("C");
+ assertThat(visits.get(2).order()).isEqualTo(2);
+ assertThat(visits.get(3).node()).isEqualTo("D");
+ assertThat(visits.get(3).order()).isEqualTo(3);
+
+ // One skip when we first encountered D from B (before C was visited)
+ assertThat(skips).hasSize(1);
+ assertThat(skips.get(0).node()).isEqualTo("D");
+ assertThat(skips.get(0).note()).contains("not all parents");
+ }
+
+ @Test
+ void returnsEmptyWhenStartNotInGraph() {
+ Map> graph = Map.of(1, List.of(2), 2, List.of(1));
+ assertThat(PredecessorConstrainedDfs.dfsRecursiveOrder(graph, 99)).isEmpty();
+ }
+
+ @Test
+ void nullSuccessorsThrows() {
+ assertThrows(IllegalArgumentException.class, () -> PredecessorConstrainedDfs.dfsRecursiveOrder(null, "A"));
+ }
+
+ @Test
+ void worksWithExplicitPredecessors() {
+ Map> successors = new HashMap<>();
+ successors.put(10, List.of(20));
+ successors.put(20, List.of(30));
+ successors.put(30, List.of());
+
+ Map> predecessors = new HashMap<>();
+ predecessors.put(10, List.of());
+ predecessors.put(20, List.of(10));
+ predecessors.put(30, List.of(20));
+
+ var events = PredecessorConstrainedDfs.dfsRecursiveOrder(successors, predecessors, 10);
+ var visitNodes = events.stream().filter(TraversalEvent::isVisit).map(TraversalEvent::node).toList();
+ assertThat(visitNodes).containsExactly(10, 20, 30);
+ }
+
+ @Test
+ void cycleProducesSkipsButNoInfiniteRecursion() {
+ Map> successors = new LinkedHashMap<>();
+ successors.put("X", List.of("Y"));
+ successors.put("Y", List.of("X")); // 2-cycle
+
+ var events = PredecessorConstrainedDfs.dfsRecursiveOrder(successors, "X");
+ // Only X is visited; encountering Y from X causes skip because Y's parent X is visited,
+ // but when recursing to Y we'd hit back to X (already visited) and stop; no infinite loop.
+ assertThat(events.stream().anyMatch(TraversalEvent::isVisit)).isTrue();
+ assertThat(events.stream().filter(TraversalEvent::isVisit).map(TraversalEvent::node)).contains("X");
+ }
+}