UVM
Why the Factory Exists
The problem the factory solves — substituting component and object types from the top without editing the environment, so one reusable env serves many specialized tests.
UVM Factory · Module 6 · Page 6.1
The Engineering Problem
You've built a reusable environment — driver, monitor, scoreboard, sequences. Now a test needs a variant: an error-injecting driver instead of the normal one, a specialised monitor, a derived transaction with extra constraints. The obvious approach is to edit the environment to construct the variant. But the moment you do that, the environment is no longer reusable — it's now the error environment, and the next test that wants the normal driver has to edit it back, or you fork a copy. Multiply that across dozens of tests and variants, and you have a maintenance disaster: forked environments that drift apart, bugs fixed in one copy but not the others.
This is the problem the factory exists to solve: substituting a type without modifying the code that creates it. If components and objects are constructed with new, the type is hard-coded at the construction site, so changing it means editing that site. If instead they're created through a factory — by asking "make me the registered type for this" — then a test can override what the factory returns, swapping in a derived type from the top, without touching the environment. One environment, closed for modification but open for substitution, serves every test and every variant. This chapter is about why that indirection matters — the pain of hard-coded types, and what the factory buys.
Why does UVM create components and objects through a factory instead of
new— what problem does that indirection solve, and how does it let one reusable environment be specialised by many tests without being edited or forked?
Motivation — the cost of hard-coded types
The factory earns its indirection by removing a class of expensive problems that hard-coded construction creates:
- Hard-coded types force you to edit (or fork) the environment. With
drv = my_driver::new(...), the driver's type is fixed in the env's source. To run an error test, you'd edit that line — but then the env is the error env, useless for the normal test. The alternative, copying the env per variant, creates duplicates that diverge: a bug fixed in one fork lingers in the others. - Reuse demands substitutability. The whole point of a reusable environment (Module 3.5) is that one env serves many contexts. Tests differ mostly in what variant components and stimulus they use — so the env must let tests swap types without being touched. Hard-coded construction makes that impossible; the factory makes it routine.
- Specialisation belongs to the test, not the env. "Inject errors," "use a constrained transaction," "swap in a debug monitor" are test decisions. They should be expressed in the test, from the top, not baked into the shared environment. The factory is the mechanism that lets a test impose its specialisation downward.
- It's the open/closed principle for testbenches. A good environment is closed for modification (you don't edit it per test) but open for extension (tests can specialise it). The factory is exactly what makes a UVM environment open/closed: extend by overriding types from above, never by editing the env.
The motivation, in one line: tests specialise a shared environment by substituting types, and hard-coded new makes that require editing or forking the env — so the factory exists to make type substitution a top-level override that leaves the environment untouched.
Mental Model
Hold the factory as ordering "the standard part" from a catalogue you can override:
The factory is ordering parts from a catalogue instead of welding a specific part in place. Imagine the environment is a machine, and at one spot it needs a "driver." The
newapproach is to weld a specific driver model into that spot — to change it, you cut it out and weld in another (edit the env). The factory approach is to put a socket there that says "install whatever the catalogue lists as 'the driver'." By default the catalogue lists the standard driver, so that's what gets installed. But anyone at the top — the test — can amend the catalogue entry: "for this run, 'the driver' means the error-injecting model." Now the same machine, unmodified, installs the error driver wherever it would have installed the standard one. The machine didn't change; the catalogue entry did. And you can amend it per run, so one machine design serves every configuration by swapping catalogue entries from the top.
So whenever you create something in UVM, you're ordering from the catalogue (type_id::create), not welding a part (new). And a test specialises the environment by amending catalogue entries (overrides) from the top — the env is the fixed machine, the overrides are the per-run catalogue amendments.
Visual Explanation — hard-coded new vs the factory
The core contrast is between welding the type in place (new) and ordering it from an overridable catalogue (the factory). One forces editing to change; the other allows substitution from the top.
The two paths diverge entirely on what it takes to change a type. With new, the type is welded into the environment's source — my_driver::new(...) names the concrete type right there — so the only way to use a different one is to edit that line, which turns the shared env into a variant-specific one (or forces a copy). With the factory, the construction site says my_driver::type_id::create(...) — it asks the factory for "the registered driver type," not a specific welded one — so a test can register an override ("build err_driver wherever my_driver would be built") and the same env, unedited, constructs the override. The difference is the location of the decision: new puts the type decision in the env (so changing it edits the env); the factory lets the type decision live in the test (so changing it leaves the env untouched). That relocation — from env to test — is the whole value.
RTL / Simulation Perspective — the problem and the solution in code
Side by side, the hard-coded approach forces an env edit; the factory approach lets a test override from the top. The env's build_phase is identical except for new vs type_id::create.
// ── THE PROBLEM: new() hard-codes the type IN the env ──
class my_env extends uvm_env;
function void build_phase(uvm_phase phase);
drv = my_driver::new("drv", this); // welded: to use err_driver, EDIT this line
endfunction
endclass
// → an error test would have to modify my_env (or fork it) — reuse broken.
// ── THE SOLUTION: create via the factory; the TEST overrides ──
class my_env extends uvm_env;
function void build_phase(uvm_phase phase);
drv = my_driver::type_id::create("drv", this); // ordered from the factory — overridable
endfunction
endclass
class error_test extends uvm_test; // specialisation lives HERE, in the test
function void build_phase(uvm_phase phase);
// amend the catalogue: "build err_driver wherever my_driver would be built"
my_driver::type_id::set_type_override(err_driver::get_type());
env = my_env::type_id::create("env", this); // the SAME env — unedited — now builds err_driver
endfunction
endclassThe two env versions differ by one token — new vs type_id::create — but the consequence is total. In the new version, my_driver is named in the env's source, so the error test has no way to substitute it without editing my_env (or copying it) — and now the normal test is broken or you maintain two diverging copies. In the factory version, the env asks the factory for the driver type, so error_test can call set_type_override in its own build_phase and the same my_env, completely unedited, constructs err_driver everywhere it would have built my_driver. The specialisation ("inject errors") lives entirely in the test, where it belongs, expressed as a catalogue amendment from the top. One env, many tests, no forks — purely because creation goes through the factory.
Verification Perspective — one environment, many tests
The factory's payoff is one shared environment serving every test, each test specialising it differently via overrides — instead of a fork per variant. This is what makes a UVM environment scale across a large test suite.
This is the factory delivering reuse at the test-suite scale. A real verification effort has many tests, and they differ mostly in which variant components and stimulus they use — the error test wants a driver that injects errors, the performance test wants a high-throughput sequence, the debug test wants an instrumented monitor. Because the environment creates everything through the factory, each test imposes its own specialisation on the same env by registering overrides in its build_phase — and the env is never edited for any of them. Contrast the alternative: without the factory, each variant needs its own copy of the env with the variant welded in, and those copies inevitably drift — a fix or feature added to one isn't in the others, and the maintenance cost grows with every test. The factory collapses that to one environment plus a small per-test override, which is precisely the reuse (Module 3.5) and open/closed design the methodology is built on.
Runtime / Execution Flow — the override decision lives at the top
The factory relocates the type decision from the construction site (deep in the env) to the test at the top. At build time, each type_id::create consults the factory, which applies any override the test registered — so the decision flows top-down.
This flow is why the factory achieves substitution-without-modification. The decision — which type to use — is made by the test, at the top, before the env builds (the test registers its overrides in its build_phase, which runs before the env's, top-down per Module 5.2). The construction is done by the env, which only ever names the default type in its type_id::create calls — it has no knowledge of any variant. The factory is the indirection that connects them: when the env's create runs, the factory checks for a registered override and returns the variant type if the test asked for one, otherwise the default. So the env constructs exactly what the test decided, while remaining ignorant of the variant and unedited. The type decision lives where it belongs (the test, which knows the intent), the construction lives where it belongs (the env, which knows the structure), and the factory is what lets those two be separate — which is the entire reason it exists.
Waveform Perspective — same env, overridden behaviour
The factory's effect is visible on the waveform: the same environment produces different pin behaviour depending on the override a test registered — the normal driver drives a clean protocol, an overridden error driver injects a fault — with no change to the env.
Same env, different behaviour — a factory override swaps the driver
10 cyclesThe waveform makes the abstract value concrete: the valid/data you see can be a clean sequence or one with an injected fault (err high, an out-of-spec beat) depending purely on which test ran the env — and the env's source is identical in both cases. The normal test registered no override, so the factory built my_driver and the protocol is clean; the error test registered an override, so the factory built err_driver in the same env, and the fault appears. The observable behaviour changed, but no environment code changed — the difference is entirely the override the test imposed from the top. This is the factory's promise rendered on the pins: behaviour specialisation without code modification. Swapping the welded part would have meant editing the machine; swapping the catalogue entry (the override) changed what the unchanged machine produced.
DebugLab — the environment that was forked five ways
Five copies of one env, drifting apart — because types were hard-coded with new
A team had five copies of their environment — env_normal, env_error, env_perf, env_debug, env_stress — each a near-duplicate differing only in which driver, monitor, or sequence it constructed. A protocol fix made in env_normal's scoreboard was missing from the other four, so four test suites silently verified against the old, buggy checking. Worse, adding a feature meant editing five files and keeping them in sync, which they routinely failed to do.
The environments were constructed with new, hard-coding the types — so the only way to get a variant was to copy the whole env and edit the welded type:
env_normal: drv = my_driver::new(...); // each env welds its own driver/monitor/seq type
env_error: drv = err_driver::new(...); // a COPY of env_normal with the driver swapped
env_perf: seq = perf_seq::new(...); // another COPY with the sequence swapped
... (5 forks total)
consequence: a fix in env_normal's scoreboard is NOT in the other 4 forks → they drift
every new feature must be edited into 5 files → sync failures, divergence
root cause: new() hard-codes types, so variants require forking+editing the env
fix: ONE factory-based env; each test registers its overrides — no forks:
error_test: my_driver::type_id::set_type_override(err_driver::get_type());The duplication wasn't a process failure; it was forced by hard-coded construction. Because new welds the type into the env, the only way to vary it was to duplicate and edit — and five copies of anything drift. The factory removes the cause: with type_id::create in one env, each variant is a small override registered by its test, so there is one env to fix and feature, and the variants are top-level deltas that can't drift from a source they share.
The tell is multiple near-duplicate environments differing only in constructed types — the signature of hard-coded new. Diagnose forced-fork situations:
- Look for env copies that differ only in types. If
env_errorisenv_normalwith onenewchanged, the duplication exists because the type was hard-coded. The factory would collapse them to one env plus an override. - Check the construction calls.
type::new(...)in an env welds the type and forces forking for variants;type::type_id::create(...)allows overrides. Pervasivenewis the root of the duplication. - Watch for drift symptoms. A fix present in one env-copy but not others, or features edited into multiple env files, signals diverging forks that a factory-based single env would eliminate.
Build one factory-based environment and specialise it with overrides:
- Create everything via the factory. Use
type_id::createfor every component and object in the env, so any type can be overridden from a test — never hard-code types withnew. - Express variants as test-level overrides, not env forks. An error test registers an
err_driveroverride; a perf test overrides the sequence. One env, many small per-test deltas — no duplicates to drift. - Keep one environment as the single source. Fix bugs and add features in the one env; every test inherits them automatically because they all use it. The factory is what makes that single-source design possible.
The one-sentence lesson: hard-coded new forces a forked, drifting environment per variant, while the factory collapses them to one env plus per-test overrides — so create through the factory and express every variant as a top-level override, never as an edited copy of the env.
Common Mistakes
- Hard-coding types with
new. Welds the type into the env, so variants require editing or forking it — the root of diverging env copies. Create everything viatype_id::createso types are overridable. - Expressing a variant by editing the shared env. Editing the env for a test's needs turns the shared env into a variant-specific one (or forces a fork). The variant belongs in the test, as an override, leaving the env untouched.
- Forking the env per test. Copying the environment to vary a type creates duplicates that drift — fixes and features diverge across copies. One factory-based env plus per-test overrides replaces all the forks.
- Putting specialisation in the env instead of the test. "Inject errors," "use this sequence" are test decisions; baking them into the env couples it to one test. The factory lets the test impose them from the top.
- Thinking the factory is about object creation mechanics. Its purpose is substitution-without-modification (reuse), not just a fancy way to call
new. The mechanics serve the reuse goal.
Senior Design Review Notes
Interview Insights
Because the factory lets you substitute a type without modifying the code that creates it, which is what makes a reusable environment specialisable by many tests. When you create a component or object with new, the concrete type is hard-coded at the construction site — so to use a variant (an error-injecting driver, a derived transaction), you'd have to edit that site, which means editing the shared environment and breaking its reusability, or forking a copy that then drifts from the original. When you create through the factory with type_id::create, the construction site asks the factory for "the registered type" rather than naming a concrete one, and a test can register an override that changes what the factory returns — so the same environment, completely unedited, builds the overridden type. The type decision moves from the environment (where new puts it) to the test (where it belongs), and the factory is the indirection that connects the test's decision to the env's construction. The result is the open/closed principle for testbenches: the environment is closed for modification but open for specialisation via overrides, so one env serves every test and variant without being edited or forked.
Exercises
- Relocate the decision. Take an env that does
drv = my_driver::new("drv", this)and an error test that needserr_driver. Rewrite so the env uses the factory and the test imposes the override — and state which file each change is in. - Spot the forced fork. A team has
env_normalandenv_errordiffering only in the drivernew. Explain why the duplication exists, the drift risk, and the single-env-plus-override design that removes it. - Who owns it? For each, say whether it belongs in the env or the test, and why: (a) the agent and scoreboard structure; (b) injecting errors; (c) using a constrained transaction; (d) connecting the monitor to the scoreboard.
- Open/closed. Explain how the factory makes an environment "closed for modification, open for extension," using a concrete override example.
Summary
- The factory exists to solve type substitution without modification: creating with
newhard-codes the type at the construction site (so variants require editing or forking the env), while creating through the factory (type_id::create) lets a test override the type from the top, leaving the env untouched. - This relocates the type decision from the environment (where
newputs it) to the test (where the intent lives) — the env names only default types and constructs whatever the test's override decided, via the factory's indirection. - The payoff is one environment, many tests: each test specialises the same env with its own overrides (error driver, perf sequence, debug monitor) instead of a forked env per variant that inevitably drifts (fixes and features diverging across copies).
- It is the open/closed principle for testbenches: the env is closed for modification (never edited per test) yet open for extension (specialised via overrides) — and behaviour can change (a clean protocol vs an injected fault) purely by the override a test registers, with no code edit.
- The durable rule of thumb: create everything through the factory (
type_id::create, nevernew) and express every variant as a top-level override in the test — so one shared environment serves the whole test suite without being edited or forked.
Next — The Factory Pattern: with why the factory exists in hand, the next chapter covers the pattern itself — the classic factory design pattern from software engineering that UVM applies, how registration and creation work, and the indirection that makes type substitution possible.