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.
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
| Variant | Control Strategy | Use Case |
|---|---|---|
| Opportunistic | Any source contributes when conditions are met | Simple, decentralized systems |
| Priority-based | Controller picks the highest-priority eligible source | Complex systems with many sources |
| Event-driven | Sources react to blackboard changes | Reactive AI systems |
| Hierarchical | Blackboards at different abstraction levels | Multi-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.
Related Resources
Mediator Pattern
Define an object that encapsulates how a set of objects interact. A behavioral design pattern for reducing chaotic dependencies.
PatternObserver Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
PatternEvent Bus Pattern
Decouple components by routing events through a central bus. A behavioral pattern for loosely coupled communication between modules.
PatternScheduler Agent Supervisor Pattern
Coordinate resilient job scheduling with a supervisor that monitors agents, restarts failures, and manages lifecycle.
PatternAgent Tool Selection Pattern
Dynamically select which tools an LLM agent can use based on the task context. Reduce token usage and improve decision quality by narrowing the tool set.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.