← ham's website case studies
Technical PM Project

Dependency-Aware Scheduler

A technical PM case study about prerequisites, blockers, rework, and launch order.

This project models work as tasks with dependencies. The code can identify which tasks are ready, prevent tasks from being completed before prerequisites are done, run a schedule step by step, and handle redo logic when finished work becomes invalid.

3 Scheduler objects
AND/OR Dependency rules modeled
Redo Rework propagation supported
PM Planning, blockers, sequencing

Project Question

How can a program decide what work can happen now, what work is blocked, and what has to be redone when a dependency changes? This is close to a product manager's planning problem: launches depend on prerequisites, some work can happen in parallel, and changes can force downstream work to be revisited.

Live Scheduler Demo

Click through the demo to see the dependency logic working. The scheduler only moves a task into Ready when its prerequisites are complete.

Launch Plan Simulation

Ready

Blocked

Completed

Ready to start. The first unblocked task is available.

Core Data Model

Code ElementWhat It RepresentsPM Meaning
tasks Every task known to the scheduler. The work items in a launch plan or roadmap.
predecessors Tasks that must be completed before another task. Prerequisites and blockers.
successors Tasks that depend on a completed task. Downstream work affected by a decision.
completed_tasks Tasks already marked as done. Current project status.

Code & PM Meaning

1. Build the Dependency Scheduler

Data Model
from collections import defaultdict

class DependencyScheduler(object):

    def __init__(self):
        self.tasks = set()
        self.successors = defaultdict(set)
        self.predecessors = defaultdict(set)
        self.completed_tasks = set()

    def add_task(self, t, dependencies):
        assert t not in self.tasks or len(self.predecessors[t]) == 0, "The task was already present."
        self.tasks.add(t)
        self.tasks.update(dependencies)
        self.predecessors[t] = set(dependencies)

        for u in dependencies:
            self.successors[u].add(t)

    def reset(self):
        self.completed_tasks = set()

    @property
    def done(self):
        return self.completed_tasks == self.tasks

    @property
    def uncompleted(self):
        return self.tasks - self.completed_tasks
Expand result and meaning

The scheduler stores each task, what it depends on, what depends on it, and what has already been completed. This is the foundation for turning a task list into a dependency-aware plan.

2. Find Available Tasks

Blockers
def scheduler_available_tasks(self):
    available = set()
    for task in self.tasks:
        if task not in self.completed_tasks:
            if self.predecessors[task].issubset(self.completed_tasks):
                available.add(task)
    return available

def scheduler_mark_completed(self, t):
    self.completed_tasks.add(t)
    newly_available = set()
    for task in self.successors[t]:
        if task not in self.completed_tasks:
            if self.predecessors[task].issubset(self.completed_tasks):
                newly_available.add(task)
    return newly_available

DependencyScheduler.available_tasks = property(scheduler_available_tasks)
DependencyScheduler.mark_completed = scheduler_mark_completed
Expand result and meaning

A task becomes available only when all of its prerequisites are complete. In PM terms, this separates work that can start now from work that is still blocked.

3. Run the Schedule Step by Step

Execution
class RunSchedule(object):

    def __init__(self, scheduler):
        self.scheduler = scheduler
        self.in_process = None

    def reset(self):
        self.scheduler.reset()
        self.in_process = None

    def step(self):
        if self.in_process is None:
            self.in_process = self.scheduler.available_tasks
        if len(self.in_process) == 0:
            return None
        t = random.choice(list(self.in_process))
        self.in_process = self.in_process - {t} | self.scheduler.mark_completed(t)
        return t

    @property
    def done(self):
        return self.scheduler.done

    def run(self):
        tasks = []
        while not self.done:
            t = self.step()
            if t is not None:
                tasks.append(t)
        return tasks
Expand result and meaning

The runner turns available-task logic into a working schedule. Each step completes one available task and unlocks any newly available work.

4. Prevent Illegal Completion

Guardrails
class IllegalCompletion(Exception):
    def __init__(self, t):
        self.message = "Not all prerequisited for %r have been completed" % t

def dependency_scheduler_mark_completed(self, t):
    if not self.predecessors[t].issubset(self.completed_tasks):
      raise IllegalCompletion(t)
    self.completed_tasks.add(t)
    return {u for u in self.successors[t]
            if self.predecessors[u].issubset(self.completed_tasks)}

DependencyScheduler.mark_completed = dependency_scheduler_mark_completed
Expand result and meaning

This adds a safeguard: a task cannot be marked done if its prerequisites are unfinished. That is a useful product-planning concept because it prevents a plan from pretending blocked work is complete.

5. Redo Downstream Work

Rework
def dependency_scheduler_redo(self, t):
    to_redo = set()
    stack = [t]
    while stack:
        current = stack.pop()
        if current not in to_redo:
            to_redo.add(current)
            stack.extend(self.successors[current])
    self.completed_tasks -= to_redo
    return to_redo

DependencyScheduler.redo = dependency_scheduler_redo
Expand result and meaning

If one task needs to be redone, all downstream tasks that depend on it may also need to be redone. This models a common launch-planning issue: changing a requirement can invalidate later work.

6. Redo Related Earlier Work

Propagation
def dependency_scheduler_cooking_redo(self, v):
    to_redo = set()
    to_redo.add(v)

    forward_tasks = set([v])
    while forward_tasks:
        current = forward_tasks.pop()
        for successor in self.successors[current]:
            if successor not in to_redo and successor in self.completed_tasks:
                to_redo.add(successor)
                forward_tasks.add(successor)

    backward_tasks = set(to_redo.copy())
    while backward_tasks:
        current = backward_tasks.pop()
        for predecessor in self.predecessors[current]:
            if predecessor not in to_redo and predecessor in self.completed_tasks:
                to_redo.add(predecessor)
                backward_tasks.add(predecessor)

    self.completed_tasks -= to_redo
    return to_redo

DependencyScheduler.cooking_redo = dependency_scheduler_cooking_redo
Expand result and meaning

This version handles a stricter rework case by propagating redo both forward and backward. It shows deeper dependency reasoning because completed prerequisites can also become invalid when the central task changes.

7. Support AND / OR Dependencies

Alternatives
class AND_OR_Scheduler(object):

    def __init__(self):
        self.tasks = set()
        self.predecessors = defaultdict(set)
        self.successors = defaultdict(set)
        self.completed_tasks = set()
        self.task_type = {}

    def add_and_task(self, t, dependencies):
        self.tasks.add(t)
        self.tasks.update(dependencies)
        self.predecessors[t] = set(dependencies)
        self.task_type[t] = "AND"

        for u in dependencies:
            self.successors[u].add(t)
            if u not in self.task_type:
                self.task_type[u] = "AND"

    def add_or_task(self, t, dependencies):
        self.tasks.add(t)
        self.tasks.update(dependencies)
        self.predecessors[t] = set(dependencies)
        self.task_type[t] = "OR"

        for u in dependencies:
            self.successors[u].add(t)
            if u not in self.task_type:
                self.task_type[u] = "AND"

    @property
    def available_tasks(self):
        available = set()
        for task in self.tasks:
            if task in self.completed_tasks:
                continue

            if self.task_type[task] == "AND":
                if self.predecessors[task].issubset(self.completed_tasks):
                    available.add(task)
            else:
                if self.predecessors[task] and not self.predecessors[task].isdisjoint(self.completed_tasks):
                    available.add(task)
                elif not self.predecessors[task]:
                    available.add(task)

        return available
Expand result and meaning

AND tasks require every prerequisite. OR tasks become available when at least one prerequisite is complete. That maps nicely to product planning when one feature has mandatory prerequisites while another can be unlocked by multiple possible paths.

What This Shows

Technical Skills

  • Object-oriented design with scheduler classes
  • Set-based dependency tracking
  • Graph-style predecessor and successor relationships
  • State management for completed and available tasks
  • Error handling for invalid task completion
  • Redo propagation across dependent work

PM Skills

  • Sequencing work based on prerequisites
  • Separating ready tasks from blocked tasks
  • Understanding downstream impact from a changed task
  • Thinking in parallel workstreams
  • Modeling alternative paths with AND/OR dependencies
  • Explaining technical logic as a planning system

Productized Version

Portfolio framing: this could be presented as a lightweight launch-planning tool.

The user would enter tasks, prerequisites, and optional alternatives. The tool would output ready tasks, blocked tasks, a suggested work order, and a redo list when a completed task changes.

FeatureBased On CodePM Value
Ready Now available_tasks Shows what the team can start immediately.
Blocked Work predecessors not completed Makes blockers visible before they delay a launch.
Suggested Sequence RunSchedule.run() Turns a dependency map into an execution order.
Redo Impact redo and cooking_redo Shows what downstream work is affected by a change.
Alternative Paths AND_OR_Scheduler Models cases where one of several prerequisites can unblock work.