Skip to content

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 new approach 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.

new hard-codes the type requiring env edits; the factory lets a test override the type without editing the envto changetypeto changetypenew: type welded in envdrv = my_driver::new(...)EDIT the envto use err_driver — breaks reuse /forces a forkfactory: ordered fromcataloguedrv = my_driver::type_id::create(...)OVERRIDE from the testset_type_override — env untouched12
Figure 1 — hard-coded new vs the factory. With new, the type is fixed at the construction site in the env, so changing it (to an error driver) requires editing the env. With the factory, the construction site asks the factory for the registered type, and a test can override what the factory returns — so the same env, unedited, builds the overridden type. new forces modification; the factory enables 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.

hard-coded new (forces editing) vs factory create (override from the top)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

The two env versions differ by one tokennew 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.

One env serves a normal test (defaults), error test (driver override), perf test (sequence override), debug test (monitor override)defaultsoverride driveroverride sequenceoverride monitorOne factory-based envcreates all via type_id::create —uneditedNormal testno overridesError testerr_driver overridePerf testperf sequence overrideDebug testdebug monitor override12
Figure 2 — one factory-based environment, many specialised tests. Because the env creates everything via the factory, each test specialises the SAME env by registering its own overrides: the normal test uses defaults, the error test overrides the driver with err_driver, the perf test overrides the sequence, the debug test overrides the monitor. One environment, unedited, serves them all. Without the factory, each variant would need an edited fork of the env that drifts from the others.

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.

The test registers overrides, the factory applies them at type_id::create, the env constructs the chosen typeTest decides (override) → factory applies → env constructsTest decides (override) → factory applies → env constructs1Test registers overridesin its build_phase, before the env builds: 'use err_driver formy_driver'.2Env builds via the factoryeach component created with type_id::create — the env names onlythe default type.3Factory applies the overridecreate consults the factory, which returns the overridden type (orthe default).4Env constructs the chosen typeerr_driver is built wherever my_driver would be — the env unaware,unedited.
Figure 3 — the type decision lives at the top, applied at construction. The test (top) registers overrides before the env builds. During build_phase, each component is created via type_id::create, which consults the factory; the factory returns the overridden type if the test registered one, else the default. So the env constructs whatever the test decided, without the env knowing or caring. The decision is the test's; the construction is the env's; the factory connects them.

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 cycles
Same env, different behaviour — a factory override swaps the driverNormal test (no override): my_driver drives a clean protocolNormal test (no overri…Error test (override): the SAME env builds err_driver, which injects a fault (err)Error test (override):…behaviour changed via a factory override — the env was never editedbehaviour changed via …clkvaliddata00A0A100B0XX00000000errt0t1t2t3t4t5t6t7t8t9
Figure 4 — the SAME environment, run by two tests. The normal test (no override) builds my_driver, which drives a clean protocol (valid/data correct). The error test registers a factory override (err_driver for my_driver), so the same env — unedited — builds err_driver, which injects a fault (an out-of-spec beat shown by err high). The behaviour differs because of the override the test registered, not because the env changed. The factory turned a code-edit into a top-level override.

The 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

Symptom

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.

Root cause

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:

why one env became five diverging forks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Diagnosis

The tell is multiple near-duplicate environments differing only in constructed types — the signature of hard-coded new. Diagnose forced-fork situations:

  1. Look for env copies that differ only in types. If env_error is env_normal with one new changed, the duplication exists because the type was hard-coded. The factory would collapse them to one env plus an override.
  2. Check the construction calls. type::new(...) in an env welds the type and forces forking for variants; type::type_id::create(...) allows overrides. Pervasive new is the root of the duplication.
  3. 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.
Prevention

Build one factory-based environment and specialise it with overrides:

  1. Create everything via the factory. Use type_id::create for every component and object in the env, so any type can be overridden from a test — never hard-code types with new.
  2. Express variants as test-level overrides, not env forks. An error test registers an err_driver override; a perf test overrides the sequence. One env, many small per-test deltas — no duplicates to drift.
  3. 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 via type_id::create so 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

  1. Relocate the decision. Take an env that does drv = my_driver::new("drv", this) and an error test that needs err_driver. Rewrite so the env uses the factory and the test imposes the override — and state which file each change is in.
  2. Spot the forced fork. A team has env_normal and env_error differing only in the driver new. Explain why the duplication exists, the drift risk, and the single-env-plus-override design that removes it.
  3. 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.
  4. 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 new hard-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 new puts 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, never new) 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.