StackPractices
advanced By Mathias Paulenko

Blackboard Pattern

A shared knowledge space where independent specialized modules collaborate to solve complex problems by contributing partial solutions.

Overview

The Blackboard Pattern gives you a shared knowledge space — the blackboard — where independent, specialized modules called Knowledge Sources work on a problem together. A source reads the board, asks itself “can I add anything useful here?”, and posts a partial solution if so.

It earns its keep when no single algorithm can crack the problem but a handful of specialized heuristics can chip away at it. A Control component runs the loop, deciding which source acts next from the current blackboard state.

I’ve seen it applied in speech recognition, NLP, image recognition, and constraint optimization — domains where the answer emerges from many small contributions rather than one decisive algorithm.

When to Use

Use the Blackboard Pattern when:

  • No single deterministic algorithm can solve the problem
  • Several specialized heuristics or algorithms can each contribute partial solutions
  • The problem space is large enough to need exploration
  • Independent modules can incrementally refine a solution
  • You want an extensible architecture for an AI or heuristic system

When to Avoid

  • Problems a single algorithm handles cleanly
  • Tight latency budgets where coordination overhead is unacceptable
  • Situations needing deterministic, predictable behavior
  • Small systems where coordination costs more than it saves

Solution

Python

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
from enum import Enum
import random

class Confidence(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

@dataclass
class Hypothesis:
    content: str
    confidence: Confidence
    source: str

@dataclass
class Blackboard:
    hypotheses: List[Hypothesis] = field(default_factory=list)
    final_solution: Optional[str] = None
    complete: bool = False

    def add_hypothesis(self, hypothesis: Hypothesis):
        self.hypotheses.append(hypothesis)

    def get_best_hypothesis(self) -> Optional[Hypothesis]:
        if not self.hypotheses:
            return None
        return max(self.hypotheses, key=lambda h: h.confidence.value)

    def set_solution(self, solution: str):
        self.final_solution = solution
        self.complete = True


class KnowledgeSource(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def can_contribute(self, blackboard: Blackboard) -> bool:
        pass

    @abstractmethod
    def contribute(self, blackboard: Blackboard):
        pass


class PatternMatcher(KnowledgeSource):
    def can_contribute(self, blackboard: Blackboard) -> bool:
        return len(blackboard.hypotheses) < 3

    def contribute(self, blackboard: Blackboard):
        bb = blackboard
        bb.add_hypothesis(Hypothesis(
            content="Pattern detected: likely function call",
            confidence=Confidence.MEDIUM,
            source=self.name
        ))


class SemanticAnalyzer(KnowledgeSource):
    def can_contribute(self, blackboard: Blackboard) -> bool:
        return any(h.source == "PatternMatcher" for h in blackboard.hypotheses)

    def contribute(self, blackboard: Blackboard):
        bb = blackboard
        bb.add_hypothesis(Hypothesis(
            content="Semantic analysis: user intent is query execution",
            confidence=Confidence.HIGH,
            source=self.name
        ))


class ConfidenceEvaluator(KnowledgeSource):
    def can_contribute(self, blackboard: Blackboard) -> bool:
        best = blackboard.get_best_hypothesis()
        return best is not None and best.confidence == Confidence.HIGH

    def contribute(self, blackboard: Blackboard):
        best = blackboard.get_best_hypothesis()
        if best:
            blackboard.set_solution(best.content)


class Controller:
    def __init__(self, blackboard: Blackboard, sources: List[KnowledgeSource]):
        self.blackboard = blackboard
        self.sources = sources

    def solve(self, max_iterations: int = 10):
        for _ in range(max_iterations):
            if self.blackboard.complete:
                break

            for source in self.sources:
                if source.can_contribute(self.blackboard):
                    source.contribute(self.blackboard)
                    break


# Usage
bb = Blackboard()
sources = [
    PatternMatcher("PatternMatcher"),
    SemanticAnalyzer("SemanticAnalyzer"),
    ConfidenceEvaluator("ConfidenceEvaluator"),
]
controller = Controller(bb, sources)
controller.solve()

print(f"Solution: {bb.final_solution}")
print(f"All hypotheses: {[h.content for h in bb.hypotheses]}")

Java

import java.util.*;

enum Confidence { LOW, MEDIUM, HIGH }

record Hypothesis(String content, Confidence confidence, String source) {}

class Blackboard {
    private final List<Hypothesis> hypotheses = new ArrayList<>();
    private String finalSolution;
    private boolean complete = false;

    public void addHypothesis(Hypothesis h) { hypotheses.add(h); }
    public List<Hypothesis> getHypotheses() { return hypotheses; }
    public boolean isComplete() { return complete; }

    public Hypothesis getBestHypothesis() {
        return hypotheses.stream()
            .max(Comparator.comparingInt(h -> h.confidence().ordinal()))
            .orElse(null);
    }

    public void setSolution(String solution) {
        this.finalSolution = solution;
        this.complete = true;
    }

    public String getFinalSolution() { return finalSolution; }
}

abstract class KnowledgeSource {
    protected final String name;
    public KnowledgeSource(String name) { this.name = name; }
    public String getName() { return name; }

    public abstract boolean canContribute(Blackboard bb);
    public abstract void contribute(Blackboard bb);
}

class PatternMatcher extends KnowledgeSource {
    public PatternMatcher() { super("PatternMatcher"); }
    public boolean canContribute(Blackboard bb) { return bb.getHypotheses().size() < 3; }
    public void contribute(Blackboard bb) {
        bb.addHypothesis(new Hypothesis("Pattern detected: likely function call", Confidence.MEDIUM, name));
    }
}

class SemanticAnalyzer extends KnowledgeSource {
    public SemanticAnalyzer() { super("SemanticAnalyzer"); }
    public boolean canContribute(Blackboard bb) {
        return bb.getHypotheses().stream().anyMatch(h -> h.source().equals("PatternMatcher"));
    }
    public void contribute(Blackboard bb) {
        bb.addHypothesis(new Hypothesis("Semantic: user intent is query execution", Confidence.HIGH, name));
    }
}

class ConfidenceEvaluator extends KnowledgeSource {
    public ConfidenceEvaluator() { super("ConfidenceEvaluator"); }
    public boolean canContribute(Blackboard bb) {
        Hypothesis best = bb.getBestHypothesis();
        return best != null && best.confidence() == Confidence.HIGH;
    }
    public void contribute(Blackboard bb) {
        Hypothesis best = bb.getBestHypothesis();
        if (best != null) bb.setSolution(best.content());
    }
}

class Controller {
    private final Blackboard blackboard;
    private final List<KnowledgeSource> sources;

    public Controller(Blackboard blackboard, List<KnowledgeSource> sources) {
        this.blackboard = blackboard;
        this.sources = sources;
    }

    public void solve(int maxIterations) {
        for (int i = 0; i < maxIterations && !blackboard.isComplete(); i++) {
            for (KnowledgeSource source : sources) {
                if (source.canContribute(blackboard)) {
                    source.contribute(blackboard);
                    break;
                }
            }
        }
    }
}

// Usage
Blackboard bb = new Blackboard();
List<KnowledgeSource> sources = List.of(
    new PatternMatcher(),
    new SemanticAnalyzer(),
    new ConfidenceEvaluator()
);
Controller controller = new Controller(bb, sources);
controller.solve(10);
System.out.println("Solution: " + bb.getFinalSolution());

JavaScript

const Confidence = {
  LOW: 1,
  MEDIUM: 2,
  HIGH: 3,
};

class Blackboard {
  constructor() {
    this.hypotheses = [];
    this.finalSolution = null;
    this.complete = false;
  }

  addHypothesis(hypothesis) {
    this.hypotheses.push(hypothesis);
  }

  getBestHypothesis() {
    if (this.hypotheses.length === 0) return null;
    return this.hypotheses.reduce((best, h) =>
      h.confidence > best.confidence ? h : best
    );
  }

  setSolution(solution) {
    this.finalSolution = solution;
    this.complete = true;
  }
}

class KnowledgeSource {
  constructor(name) {
    this.name = name;
  }

  canContribute(blackboard) {
    throw new Error('Must implement canContribute');
  }

  contribute(blackboard) {
    throw new Error('Must implement contribute');
  }
}

class PatternMatcher extends KnowledgeSource {
  constructor() { super('PatternMatcher'); }
  canContribute(bb) { return bb.hypotheses.length < 3; }
  contribute(bb) {
    bb.addHypothesis({
      content: 'Pattern detected: likely function call',
      confidence: Confidence.MEDIUM,
      source: this.name,
    });
  }
}

class SemanticAnalyzer extends KnowledgeSource {
  constructor() { super('SemanticAnalyzer'); }
  canContribute(bb) {
    return bb.hypotheses.some(h => h.source === 'PatternMatcher');
  }
  contribute(bb) {
    bb.addHypothesis({
      content: 'Semantic analysis: user intent is query execution',
      confidence: Confidence.HIGH,
      source: this.name,
    });
  }
}

class ConfidenceEvaluator extends KnowledgeSource {
  constructor() { super('ConfidenceEvaluator'); }
  canContribute(bb) {
    const best = bb.getBestHypothesis();
    return best && best.confidence === Confidence.HIGH;
  }
  contribute(bb) {
    const best = bb.getBestHypothesis();
    if (best) bb.setSolution(best.content);
  }
}

class Controller {
  constructor(blackboard, sources) {
    this.blackboard = blackboard;
    this.sources = sources;
  }

  solve(maxIterations = 10) {
    for (let i = 0; i < maxIterations && !this.blackboard.complete; i++) {
      for (const source of this.sources) {
        if (source.canContribute(this.blackboard)) {
          source.contribute(this.blackboard);
          break;
        }
      }
    }
  }
}

// Usage
const bb = new Blackboard();
const sources = [
  new PatternMatcher(),
  new SemanticAnalyzer(),
  new ConfidenceEvaluator(),
];
const controller = new Controller(bb, sources);
controller.solve();
console.log('Solution:', bb.finalSolution);

Explanation

Three components make up the Blackboard Pattern:

  • Blackboard: the shared data structure holding the problem state and accumulated hypotheses
  • Knowledge Sources: independent modules that read the blackboard, check whether their conditions apply, and post partial solutions
  • Controller: the loop that picks which source acts next

Sources never talk to each other; they only read and write the blackboard. Want to extend the system? Drop in a new source — nothing else needs to change.

Blackboard architecture: the controller inspects blackboard state, activates knowledge sources, and sources read and write hypotheses until a solution converges.

For a contrasting approach, the Mediator pattern centralizes coordination between known objects rather than broadcasting through shared state, and the Event Bus pattern routes explicit events instead of letting sources inspect shared hypotheses.

Variants

VariantControl StrategyUse Case
OpportunisticAny source contributes when conditions are metSimple, decentralized systems
Priority-basedController picks the highest-priority eligible sourceComplex systems with many sources
Event-drivenSources react to blackboard changesReactive AI systems
HierarchicalBlackboards at different abstraction levelsMulti-stage problem solving

What Works

  • Keep sources independent. If sources call each other directly, you’ve built a mediator, not a blackboard.
  • Give the controller a priority queue. Scanning sources in order wastes cycles — pick whichever source looks most promising.
  • Score hypothesis confidence. Confidence levels give the controller something real to decide with.
  • Cap iterations. There’s no convergence guarantee, so you need an escape hatch.
  • Log every contribution. When a blackboard system goes wrong, the contribution trace is how you find it.

A runnable version of this pattern — three knowledge sources classifying text on a shared blackboard — lives in the companion repository.

Common Mistakes

  • Tight coupling between sources. Direct communication defeats the pattern’s purpose.
  • Missing termination conditions. No max-iterations cap, no completion check — the loop just keeps going.
  • Overwriting high-confidence hypotheses. A new contribution shouldn’t blindly replace a better one.
  • Monolithic controller. A controller that does everything becomes the bottleneck and the single point of failure.
  • Ignoring race conditions. Multi-threaded implementations need synchronized access to the blackboard.

Real-World Examples

Speech Recognition Systems

Speech recognition is the classic blackboard application: acoustic models, language models, pronunciation dictionaries, and contextual analyzers each post hypotheses about what was said, and the system converges on the most likely transcription.

Image Recognition Pipelines

In computer vision, edge detectors, shape recognizers, and semantic classifiers each post partial interpretations of an image to a shared board — the same idea wearing different clothes.

Optimization Solvers

Constraint solvers used in scheduling and routing run several heuristics that iteratively refine a shared solution space — blackboard in everything but name.

Frequently Asked Questions

What's the difference between the Blackboard and Pipeline patterns?

Pipelines move data linearly, stage to stage — each step only sees the previous step's output. A blackboard source contributes whenever it finds something to add, reading whatever hypotheses happen to be on the board — order doesn't exist.

How does the controller decide which source to activate?

Usually priority ordering, confidence thresholds, or auction-style bidding where each source scores its own ability to contribute.

Is the Blackboard pattern suitable for real-time systems?

It's a hard fit. Coordination overhead makes latency unpredictable, so real-time systems usually prefer deterministic pipelines or state machines.

Can I use Blackboard in a small project?

You can, but it rarely pays off — with few components the coordination overhead outweighs the flexibility. Start simple and introduce it when the problem actually needs competing hypotheses.

How is Blackboard different from Mediator?

Mediator centralizes communication between objects that know about each other. Blackboard decouples them through shared state and opportunistic contribution — sources don't even know other sources exist.