UVM Factory: Registration, create() and Overrides
The UVM factory is the single idea that makes a UVM testbench reusable. Instead of hard-coding which class gets built where, you register your classes, ask the factory to build them by type, and then swap one type for another from the test, without touching the environment code. For an ECE student moving from plain Verilog testbenches to industrial verification, this is the mental shift that unlocks the whole methodology. This tutorial builds the idea from the ground up: registration, create() versus new(), and type and instance overrides.
A note on running these examples: the UVM code blocks below are written for a UVM-capable simulator such as industry-standard simulation tools, VCS, or industry-standard simulators (or EDA Playground in the browser) and show canonical UVM structure. They depend on the UVM base-class library, so they will not run on free tools like Icarus Verilog or Verilator. The one block marked as runnable is plain SystemVerilog with no UVM library, included to make the underlying factory idea concrete.
Why a factory at all?
Imagine your environment builds a driver like this: drv = new("drv", this);. That line names a concrete class. If a test later needs an error-injecting driver instead, you would have to edit the environment. Multiply that across dozens of components and hundreds of tests and the testbench becomes rigid.
The factory breaks that coupling. The environment says “build me a driver,” the factory decides which concrete class to instantiate, and a test can change that decision in one line. The environment never changes. This is the classic factory design pattern applied to hardware verification.
The concept, in plain SystemVerilog
Before any UVM syntax, here is the whole idea in self-contained SystemVerilog that needs no library. A tiny factory builds a packet by name. The requesting code always asks for the same thing, yet after we register an override the factory hands back a different type. The handle type the caller holds never changes.
module tb;
class packet;
string kind;
function new();
kind = "packet";
endfunction
endclass
class err_packet extends packet;
function new();
super.new();
kind = "err_packet";
endfunction
endclass
class packet_factory;
string override_name = "";
function packet make(string name);
packet p;
err_packet e;
string eff;
eff = name;
if (override_name != "") eff = override_name;
if (eff == "err_packet") begin
e = new();
p = e;
end else begin
p = new();
end
return p;
endfunction
function void set_override(string with_name);
override_name = with_name;
endfunction
endclass
packet_factory f;
packet a, b;
initial begin
f = new();
$display("Request 'packet' with no override:");
a = f.make("packet");
$display(" factory produced a '%s'", a.kind);
$display("Register override packet -> err_packet, then request 'packet' again:");
f.set_override("err_packet");
b = f.make("packet");
$display(" factory produced a '%s'", b.kind);
$finish;
end
endmoduleRequest 'packet' with no override:
factory produced a 'packet'
Register override packet -> err_packet, then request 'packet' again:
factory produced a 'err_packet'
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmphn2ym377/tb.sv:55: $finish called at 0 (1s)
Notice the caller holds a base packet handle both times. The factory, not the caller, decides the concrete class. The real UVM factory does exactly this, but with proper type identifiers instead of string guesswork, and it is created and wired for you.
Step 1: registering your classes
Before the factory can build a class, it has to know the class exists. You register it with a utility macro. There are two, one for components and one for objects.
`uvm_component_utils(T)registers a class derived fromuvm_component(drivers, monitors, agents, environments, tests). Components live for the whole simulation and sit in the UVM hierarchy.`uvm_object_utils(T)registers a class derived fromuvm_object(transactions, sequence items, configuration objects). Objects are transient data that flow through the testbench.
These macros do more than mark the class. They create a nested helper type called type_id and register a proxy (a singleton the factory stores) so the factory can later construct your class on demand. They also generate boilerplate such as get_type_name().
// A transaction: derives from uvm_object family
class my_packet extends uvm_sequence_item;
rand bit [7:0] addr;
rand bit [7:0] data;
`uvm_object_utils(my_packet)
function new(string name = "my_packet");
super.new(name);
endfunction
endclass
// A component: derives from uvm_component
class my_driver extends uvm_driver #(my_packet);
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclassTwo constructor rules follow directly from the base classes. An object constructor takes one string name argument (with a default). A component constructor takes string name and uvm_component parent. The factory relies on these exact signatures, which is why every registered class must follow them.
Step 2: create() vs new()
This is the question every beginner asks. Both ultimately construct an object, but they are not interchangeable.
new() is the ordinary SystemVerilog constructor. It builds the exact class you named. It is invisible to the factory, so an override can never redirect it. create() asks the factory to build the registered type, which means an override can redirect it.
| Aspect | new() | create() |
|---|---|---|
| Who builds it | SystemVerilog directly | The UVM factory |
| Can be overridden? | No | Yes |
| Typical use | Inside constructors, low-level helpers | Building components and transactions in the testbench |
| Syntax | d = new("d", this); | d = my_driver::type_id::create("d", this); |
The rule in practice: build every component and every transaction with create(). Reserve plain new() for the constructor itself and for simple local helpers that you never intend to override. Here is a component building its child the correct way, inside build_phase:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Factory builds the driver; an override can swap it later.
drv = my_driver::type_id::create("drv", this);
endfunction
endclassFor a transaction there is no parent, so the second argument is dropped:
my_packet pkt;
pkt = my_packet::type_id::create("pkt");Step 3: type overrides
A type override tells the factory: “everywhere someone asks you to build type A, build type B instead.” It is global across the testbench. You typically do this in a test’s build_phase, before the environment is built, so the new type is in effect when components are created.
Suppose err_driver extends my_driver and injects protocol errors. A test can swap it in without the agent or environment knowing:
class err_test extends uvm_test;
`uvm_component_utils(err_test)
my_env env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
// Any create() of my_driver now yields an err_driver.
set_type_override_by_type(my_driver::get_type(),
err_driver::get_type());
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
endclassThere is a convenient equivalent written on the class itself: my_driver::type_id::set_type_override(err_driver::get_type());. A string-based form, set_type_override("my_driver", "err_driver"), also exists but the type-based form is safer because a typo becomes a compile error rather than a silent miss.
Step 4: instance overrides
A type override hits every instance. Sometimes you want to change only one component, identified by its position in the hierarchy. That is an instance override. It takes a path string and wins over any type override for that specific instance.
// Only the driver at this exact path becomes err_driver.
// Every other my_driver in the testbench is unchanged.
set_inst_override_by_type("env.agent1.drv",
my_driver::get_type(),
err_driver::get_type());The path uses the instance names you passed to create(), joined by dots. Because the override is registered against that path, it only fires when the factory builds a component matching it. Instance overrides are perfect for targeted scenarios: one faulty agent on a multi-agent bus, while the rest stay well-behaved.
Putting it together
The factory flow is four moves. Register classes with `uvm_component_utils or `uvm_object_utils. Build everything with type_id::create() instead of new(). Replace types globally with a type override, or surgically with an instance override, from the test. The environment is written once and reused across every test, because the test, not the environment, decides which concrete classes appear.
One habit will save you hours of debugging: if an override “does nothing,” check that the component was built with create() and not new(), and that the override was registered before the target was constructed. Those two mistakes account for the large majority of factory confusion.
Quick check
Which macro registers a transaction (sequence item) with the factory?
`uvm_object_utils(T). Transactions derive from the uvm_object family, so they use the object macro. Components (drivers, monitors, agents, tests) use `uvm_component_utils(T) instead.
Why can an override redirect create() but never new()?
create() asks the factory to build the registered type, so the factory can consult its override table and substitute a different class. new() is the plain SystemVerilog constructor that builds the exact named class directly, bypassing the factory entirely, so no override can intercept it.
What is the difference between a type override and an instance override?
A type override replaces a class everywhere it is created, across the whole testbench. An instance override replaces the class only for a component at one specific hierarchical path (for example "env.agent1.drv"), leaving all other instances of that type unchanged. The instance override takes priority for the path it names.
Your test sets a type override but the behavior does not change. What are the two most likely causes?
First, the target component was built with new() instead of type_id::create(), so the factory never had a chance to substitute it. Second, the override was registered after the component was already constructed; it must be set earlier, typically in the test’s build_phase before super.build_phase() builds the environment.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses