Graph Algorithms for Backend Engineers: BFS, DFS, and Dijkstra

Graph algorithms appear more often in backend engineering than many engineers expect: dependency resolution, shortest path in routing systems, cycle detection in task schedulers, and reachability in a

Introduction#

Graph algorithms appear more often in backend engineering than many engineers expect: dependency resolution, shortest path in routing systems, cycle detection in task schedulers, and reachability in access control systems. This post covers BFS, DFS, and Dijkstra with practical backend examples.

Graph Representation#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from collections import defaultdict
from typing import TypeAlias

# Adjacency list: most common for sparse graphs
Graph: TypeAlias = dict[str, list[str]]
WeightedGraph: TypeAlias = dict[str, list[tuple[str, float]]]

# Example: service dependency graph
dependencies: Graph = {
    "api":        ["auth", "database", "cache"],
    "auth":       ["database"],
    "database":   [],
    "cache":      [],
    "worker":     ["database", "queue"],
    "queue":      [],
}

# Example: network topology with latency weights
network: WeightedGraph = {
    "us-east-1": [("eu-west-1", 80), ("ap-south-1", 200)],
    "eu-west-1": [("us-east-1", 80), ("ap-south-1", 150)],
    "ap-south-1": [("us-east-1", 200), ("eu-west-1", 150)],
}

BFS explores nodes level by level. Use for: shortest path in unweighted graphs, finding all nodes reachable within N hops, level-order traversal.

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
from collections import deque

def bfs(graph: Graph, start: str) -> list[str]:
    """Return nodes in BFS order from start."""
    visited = {start}
    queue = deque([start])
    order = []

    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return order

def shortest_path_bfs(graph: Graph, start: str, end: str) -> list[str] | None:
    """Find shortest path (fewest hops) between start and end."""
    if start == end:
        return [start]

    visited = {start}
    queue = deque([[start]])  # queue of paths

    while queue:
        path = queue.popleft()
        node = path[-1]
        for neighbor in graph.get(node, []):
            if neighbor == end:
                return path + [neighbor]
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(path + [neighbor])
    return None  # no path

# Find all services reachable from "api" (blast radius of an API outage)
reachable = bfs(dependencies, "api")
print(f"API depends on: {reachable}")

DFS explores as deep as possible before backtracking. Use for: cycle detection, topological sort, connected components.

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
def dfs_iterative(graph: Graph, start: str) -> list[str]:
    """Iterative DFS (avoids recursion stack overflow for large graphs)."""
    visited = set()
    stack = [start]
    order = []

    while stack:
        node = stack.pop()
        if node in visited:
            continue
        visited.add(node)
        order.append(node)
        for neighbor in reversed(graph.get(node, [])):
            if neighbor not in visited:
                stack.append(neighbor)
    return order

def has_cycle(graph: Graph) -> bool:
    """Detect a cycle using DFS with three states."""
    WHITE, GRAY, BLACK = 0, 1, 2  # unvisited, in progress, done
    color = {node: WHITE for node in graph}

    def dfs(node: str) -> bool:
        color[node] = GRAY
        for neighbor in graph.get(node, []):
            if color[neighbor] == GRAY:
                return True  # back edge = cycle
            if color[neighbor] == WHITE and dfs(neighbor):
                return True
        color[node] = BLACK
        return False

    return any(dfs(node) for node in graph if color[node] == WHITE)

def topological_sort(graph: Graph) -> list[str]:
    """Return nodes in topological order (dependency before dependent)."""
    if has_cycle(graph):
        raise ValueError("Graph has a cycle — cannot topologically sort")

    visited = set()
    result = []

    def dfs(node: str):
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                dfs(neighbor)
        result.append(node)

    for node in graph:
        if node not in visited:
            dfs(node)

    return result[::-1]  # reverse post-order = topological order

# Determine safe startup order for services
startup_order = topological_sort(dependencies)
print(f"Start services in order: {startup_order}")
# Output: ['database', 'cache', 'auth', 'queue', 'api', 'worker']

Dijkstra: Shortest Path with Weights#

Dijkstra finds the shortest weighted path from a source to all other nodes.

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
import heapq
from typing import Optional

def dijkstra(
    graph: WeightedGraph,
    start: str
) -> tuple[dict[str, float], dict[str, Optional[str]]]:
    """
    Returns:
    - distances: shortest distance from start to each node
    - predecessors: for path reconstruction
    """
    distances: dict[str, float] = {node: float("inf") for node in graph}
    distances[start] = 0
    predecessors: dict[str, Optional[str]] = {node: None for node in graph}

    # Min-heap: (distance, node)
    heap = [(0, start)]

    while heap:
        dist, node = heapq.heappop(heap)

        # Skip if we already found a shorter path
        if dist > distances[node]:
            continue

        for neighbor, weight in graph.get(node, []):
            new_dist = dist + weight
            if new_dist < distances[neighbor]:
                distances[neighbor] = new_dist
                predecessors[neighbor] = node
                heapq.heappush(heap, (new_dist, neighbor))

    return distances, predecessors

def shortest_path(
    graph: WeightedGraph,
    start: str,
    end: str
) -> tuple[float, list[str]]:
    """Return (total cost, path) from start to end."""
    distances, predecessors = dijkstra(graph, start)
    if distances[end] == float("inf"):
        return float("inf"), []

    path = []
    node: Optional[str] = end
    while node is not None:
        path.append(node)
        node = predecessors[node]
    return distances[end], path[::-1]

# Find lowest-latency routing between regions
cost, path = shortest_path(network, "us-east-1", "ap-south-1")
print(f"Best path: {''.join(path)} (latency: {cost}ms)")

Practical Application: Permission Graph#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def can_access(permission_graph: Graph, user_role: str, required_permission: str) -> bool:
    """Check if user_role can reach required_permission via role inheritance."""
    # BFS from user_role through role hierarchy
    visited = {user_role}
    queue = deque([user_role])

    while queue:
        role = queue.popleft()
        if role == required_permission:
            return True
        for parent_role in permission_graph.get(role, []):
            if parent_role not in visited:
                visited.add(parent_role)
                queue.append(parent_role)
    return False

role_hierarchy = {
    "admin": ["editor", "viewer"],
    "editor": ["viewer"],
    "viewer": [],
}

print(can_access(role_hierarchy, "admin", "viewer"))  # True
print(can_access(role_hierarchy, "viewer", "editor"))  # False

Conclusion#

BFS for shortest paths in unweighted graphs and level-order exploration. DFS for cycle detection, topological sort, and connected components. Dijkstra for shortest weighted paths — the basis for routing, CDN edge selection, and service mesh traffic management. The adjacency list representation with a visited set handles most production use cases efficiently.

Contents