The short answer: Design patterns are reusable solutions to recurring testbench problems. The ones that matter most in SystemVerilog are the factory (create objects so a test can substitute a derived class), callbacks (hooks a test attaches to inject errors or add checks), the observer (one monitor broadcasting to many subscribers), configuration objects, singletons, and strategy or policy classes. UVM is largely these patterns packaged as a library.
You don’t need to study software design patterns in depth to write good testbenches, but a handful of them solve problems every verification engineer runs into: “how do I inject an error without editing the driver?” or “how do I send monitor output to three places?” This post combines my older posts on design patterns and best practices.
- The six patterns
- Factory
- Callbacks
- Observer
- Configuration objects and singletons
- Strategy
- Best practices that go with them
Table of Contents
Part 1: The six patterns

Part 2: Factory
class txn_factory;
static function txn create(string kind);
case (kind)
"err": begin err_txn e = new(); return e; end
default: begin txn t = new(); return t; end
endcase
endfunction
endclass
// the generator always calls the factory; the test picks the kind
t = txn_factory::create(cfg.txn_kind);
The environment never names the concrete class, so tests can change behaviour without editing it. UVM’s factory does this generically with type_id::create() and type overrides.
Part 3: Callbacks
virtual class driver_cb;
virtual task pre_drive(ref txn t); endtask
endclass
class corrupt_cb extends driver_cb;
virtual task pre_drive(ref txn t);
if ($urandom_range(99) < 5) t.data ^= 32'h1; // 5% bit flips
endtask
endclass
class driver;
driver_cb cbs[$];
task drive_one(txn t);
foreach (cbs[i]) cbs[i].pre_drive(t);
// ... drive t ...
endtask
endclass
The driver stays clean; error injection lives in the test.
Part 4: Observer
A monitor shouldn’t know who consumes its transactions. Give it a list of subscribers (or, in UVM, an analysis port) and let the environment connect the scoreboard, coverage and any test-specific checkers. Adding a new checker then needs no change to the monitor.
Part 5: Configuration objects and singletons
class env_cfg;
rand int unsigned num_txns = 1000;
bit has_coverage = 1;
bit active = 1;
virtual stream_if vif;
endclass
Pass one configuration object down instead of dozens of separate knobs. A singleton (a class with a static get() that always returns the same instance) is useful for truly global things like a report counter, but overusing it creates hidden dependencies.
Part 6: Strategy
When the algorithm varies, such as how a scoreboard compares packets (in order, out of order, by ID) or how an arbiter model picks a winner, put each version in its own class with a common base and choose one in the configuration.
Part 7: Best practices that go with them
- Program to base classes and virtual methods, so extensions plug in.
- Keep components independent: communicate through ports, mailboxes or analysis ports, not hierarchical references.
- Put everything a test might change into configuration objects.
- Prefer composition (a component that has a strategy) over deep inheritance trees.
- Don’t add a pattern until you need it; simple testbenches should stay simple.
Practise this on VLSI Forge
I built VLSI Forge so you can write RTL in your browser, run it on a real simulator and check every signal in the waveform. Free, nothing to install.
Previous in my SystemVerilog series: DPI. That’s the end of my SystemVerilog series. Next, I’d move on to UVM.
FAQ
Why are design patterns useful in verification?
They let you change testbench behaviour (stimulus types, error injection, checking) from a test without editing shared components, which makes environments reusable.
What is the factory pattern in SystemVerilog?
A way to create objects through a central function or registry, so a test can substitute a derived class. UVM’s factory is the standard implementation.
What is a callback in a testbench?
A hook in a component that calls user-supplied code at a defined point, for example just before a transaction is driven.
Do I need these if I use UVM?
UVM provides most of them for you, but understanding the patterns makes UVM code much easier to read and debug.
