Abstract Factory Pattern in UVM: Keeping VIP Families Consistent
The Factory post covered three GoF variants — Simple Factory, Factory Method, and Abstract Factory — and promised we'd come back to the third one. The Prototype post wrapped up the Creational series and pointed toward Structural patterns. Before we move on, there's one Creational pattern from that very first post that deserves its own home, because it solves a problem no other pattern in this series can touch: keeping a whole family of VIP components consistent with each other.
This post is that home. We'll anchor the discussion in a concrete AMBA family — AXI4, ACE, and ACE-Lite — because the mismatched-family bug it solves is one of the nastiest silent failures in coherency verification.
- The Problem: The Mismatched-Family Bug
- Gang of Four: The Abstract Factory Pattern
- UVM Implementation: AXI4/ACE/ACE-Lite VIP Factories
- Family Consistency: Compile-Time vs Runtime
- Pattern Combinations: Singleton, Builder, and config_db
- Quick Reference
The Problem: The Mismatched-Family Bug
You're verifying a coherent subsystem with two masters and a coherent interconnect. CPU-side traffic is full ACE — the master has a cache, snoops are routed back through the AC channel, and the interconnect resolves coherency. The GPU is I/O-coherent, so it speaks ACE-Lite — it can issue cache-maintenance operations and snoop other caches, but it has no cache of its own to be snooped. There's also a debug master that only does AXI4 register reads — no coherency at all.
Three masters. Three protocol variants. One env. Here's the build phase a junior engineer might write:
class coherent_env extends uvm_env;
`uvm_component_utils(coherent_env)
ace_master cpu_master;
ace_lite_master gpu_master;
axi4_master debug_master;
axi4_monitor interconnect_monitor; // <-- uh oh
coherency_scoreboard scbd;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
cpu_master = ace_master::type_id::create("cpu_master", this);
gpu_master = ace_lite_master::type_id::create("gpu_master", this);
debug_master = axi4_master::type_id::create("debug_master", this);
interconnect_monitor = axi4_monitor::type_id::create("interconnect_monitor", this);
scbd = coherency_scoreboard::type_id::create("scbd", this);
endfunction
endclass
Compiles cleanly. Elaborates. Simulates. Coverage stays green. And it's silently broken.
The monitor on the coherent interconnect is axi4_monitor. It decodes AR/AW/R/W/B perfectly. It has no idea what AC/CR/CD are — those are ACE's snoop channels, and the AXI4 monitor's interface doesn't even declare them. Every snoop transaction the interconnect issues is invisible to the scoreboard. The MOESI state machine that's supposed to be checked stays a black box. False coverage. False confidence. Bug ships.
The naive fix is to manually pair the right components every time you build an env. But that just relocates the problem:
// Manual pairing — works until someone forgets
if (cfg.coherent) begin
monitor = ace_monitor::type_id::create("monitor", this);
scbd = ace_scoreboard::type_id::create("scbd", this);
end
else if (cfg.io_coherent) begin
monitor = ace_lite_monitor::type_id::create("monitor", this);
scbd = ace_lite_scoreboard::type_id::create("scbd", this);
end
else begin
monitor = axi4_monitor::type_id::create("monitor", this);
scbd = axi4_scoreboard::type_id::create("scbd", this);
end
Three families × four roles (master, slave, monitor, scoreboard) = twelve create() calls scattered across an if/else ladder, and the rule "all four must come from the same family" lives only in the engineer's head. The next person who adds a checker for the new ACE5-Lite extension will copy half the branches and forget the rest. The compiler will not save you. The simulator will not save you. The reviewer might — but only on a good day.
The core issue: you have multiple families of related components, and the consistency rule between members of a family is implicit. You need a pattern that bundles the family together so that picking ACE means picking the ACE master, the ACE monitor, the ACE scoreboard, and the ACE coherency checker — and never lets you mix in an AXI4 monitor by accident.
There's a pattern for this — and the Factory post hinted at it.
Gang of Four: The Abstract Factory Pattern
"Provide an interface for creating families of related or dependent objects without specifying their concrete classes."
— Design Patterns: Elements of Reusable Object-Oriented Software (Gamma et al., 1994)
The key phrase is families of related or dependent objects. Factory Method (which UVM's type_id::create() implements at the per-class level) makes a single product type. Abstract Factory makes a family — every product the factory produces is guaranteed to belong to the same family by construction.
classDiagram
class AbstractFactory {
<<interface>>
+createMaster()* : AbstractMaster
+createMonitor()* : AbstractMonitor
+createScoreboard()* : AbstractScoreboard
}
class Axi4Factory {
+createMaster() : Axi4Master
+createMonitor() : Axi4Monitor
+createScoreboard() : Axi4Scoreboard
}
class AceFactory {
+createMaster() : AceMaster
+createMonitor() : AceMonitor
+createScoreboard() : AceScoreboard
}
class AceLiteFactory {
+createMaster() : AceLiteMaster
+createMonitor() : AceLiteMonitor
+createScoreboard() : AceLiteScoreboard
}
class AbstractMaster {
<<interface>>
}
class AbstractMonitor {
<<interface>>
}
class AbstractScoreboard {
<<interface>>
}
AbstractFactory <|-- Axi4Factory
AbstractFactory <|-- AceFactory
AbstractFactory <|-- AceLiteFactory
AbstractFactory ..> AbstractMaster : creates
AbstractFactory ..> AbstractMonitor : creates
AbstractFactory ..> AbstractScoreboard : creates
The roles map cleanly onto verification:
- AbstractFactory — A base class that promises "I can build a complete VIP family." In our world, that's a
vip_factory_basewith virtualcreate_master(),create_monitor(),create_scoreboard(),create_checker()methods. - ConcreteFactory — One per family.
axi4_vip_factory,ace_vip_factory,ace_lite_vip_factory. Each overrides every create method to return the matching family member. - AbstractProduct — A base class per role.
axi_master_base,axi_monitor_base, etc. All concrete masters extendaxi_master_base, so the env can hold them through the base handle. - ConcreteProduct — The actual classes.
ace_master extends axi_master_base,ace_monitor extends axi_monitor_base, and so on.
The env never names a concrete class. It holds an AbstractFactory handle and asks it for parts. Whichever concrete factory was installed determines the whole family, and the env can't mix.
How It Differs From Factory Method
Both patterns produce objects through an abstract create* interface. The difference is in what gets produced.
| Aspect | Factory Method | Abstract Factory |
|---|---|---|
| Number of products | One per factory method | A whole family per factory |
| Consistency guarantee | None — each call is independent | Built-in — all products come from the same family |
| Typical use case | "Give me a transaction object" | "Give me a whole VIP family" |
| UVM analog | type_id::create() | A custom factory class hierarchy |
| Adds value when | You want type substitution per class | You want type substitution per family |
| Anti-pattern it prevents | Hardcoded class names | Mixing components from different families |
UVM's built-in factory is a Factory Method on steroids — it works wonderfully when you want to substitute one class at a time. But the moment you have inter-component invariants (a snoop-aware monitor must be paired with a snoop-aware scoreboard), you need Abstract Factory layered on top. UVM's factory will happily let you override axi_monitor to ace_monitor while leaving axi_scoreboard alone — that's a feature for Factory Method and a bug for family consistency.
UVM Implementation: AXI4/ACE/ACE-Lite VIP Factories
Time to build it. We need:
- Abstract product base classes (one per role).
- Concrete products extending each base (one per family per role).
- An abstract factory base class with virtual
create_*methods. - A concrete factory per family.
- An env that takes a factory handle and uses it to build its components.
The Abstract Products
The bases declare only the interface the env relies on. Concrete products extend them and add protocol-specific behavior.
virtual class axi_master_base extends uvm_component;
`uvm_component_utils(axi_master_base)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
virtual class axi_monitor_base extends uvm_component;
`uvm_component_utils(axi_monitor_base)
uvm_analysis_port #(uvm_sequence_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
endclass
virtual class axi_scoreboard_base extends uvm_component;
`uvm_component_utils(axi_scoreboard_base)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
virtual class coherency_checker_base extends uvm_component;
`uvm_component_utils(coherency_checker_base)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
Marking each base virtual is deliberate — these are abstract types. You should never instantiate axi_master_base directly; the only legal masters are axi4_master, ace_master, and ace_lite_master. The compiler enforces this.
The Abstract Factory
The factory base declares the contract. Concrete factories must override every method.
virtual class vip_factory_base extends uvm_object;
`uvm_object_utils(vip_factory_base)
function new(string name = "vip_factory_base");
super.new(name);
endfunction
pure virtual function axi_master_base create_master (string name, uvm_component parent);
pure virtual function axi_monitor_base create_monitor (string name, uvm_component parent);
pure virtual function axi_scoreboard_base create_scoreboard(string name, uvm_component parent);
pure virtual function coherency_checker_base create_checker (string name, uvm_component parent);
pure virtual function string family_name();
endclass
The pure virtual keyword forces every concrete subclass to implement these methods. A factory that forgets create_checker() will fail to compile — exactly the discipline we want.
A Concrete Factory: ACE
class ace_vip_factory extends vip_factory_base;
`uvm_object_utils(ace_vip_factory)
function new(string name = "ace_vip_factory");
super.new(name);
endfunction
virtual function axi_master_base create_master(string name, uvm_component parent);
return ace_master::type_id::create(name, parent);
endfunction
virtual function axi_monitor_base create_monitor(string name, uvm_component parent);
return ace_monitor::type_id::create(name, parent);
endfunction
virtual function axi_scoreboard_base create_scoreboard(string name, uvm_component parent);
return ace_scoreboard::type_id::create(name, parent);
endfunction
virtual function coherency_checker_base create_checker(string name, uvm_component parent);
return ace_coherency_checker::type_id::create(name, parent);
endfunction
virtual function string family_name();
return "ACE";
endfunction
endclass
Notice what each create_* method does internally: it calls type_id::create(). That's the UVM factory (Factory Method). So an ace_vip_factory is layered on top of the existing UVM factory — you get the Abstract Factory's family-consistency guarantee plus the UVM factory's per-class override flexibility. Want to swap ace_monitor for an instrumented ace_monitor_with_coverage? Use UVM's set_type_override as usual. The family-level decision is still made by the Abstract Factory.
The axi4_vip_factory and ace_lite_vip_factory follow the same shape, returning their family's concrete classes. I'll skip the boilerplate.
The Env Becomes Trivial
class coherent_env extends uvm_env;
`uvm_component_utils(coherent_env)
vip_factory_base factory;
axi_master_base master;
axi_monitor_base monitor;
axi_scoreboard_base scbd;
coherency_checker_base checker;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(vip_factory_base)::get(this, "", "factory", factory))
`uvm_fatal("CFG", "No VIP factory provided to coherent_env")
master = factory.create_master ("master", this);
monitor = factory.create_monitor ("monitor", this);
scbd = factory.create_scoreboard("scbd", this);
checker = factory.create_checker ("checker", this);
`uvm_info("ENV", $sformatf("Built %s family VIP", factory.family_name()), UVM_LOW)
endfunction
endclass
Look at what's gone: every concrete class name has disappeared. Look at what's impossible: you cannot, by construction, build an env where the monitor is axi4_monitor and the scoreboard is ace_scoreboard. The factory hands you four objects that all belong to the same family, every time.
sequenceDiagram
participant Test
participant ConfigDB as uvm_config_db
participant Env as coherent_env
participant Factory as ace_vip_factory
participant UvmFactory as uvm_factory
Test->>Test: factory_h = ace_vip_factory::type_id::create("factory_h")
Test->>ConfigDB: set("coherent_env", "factory", factory_h)
Test->>Env: build_phase()
Env->>ConfigDB: get("factory") returns ace_vip_factory
Env->>Factory: create_master("master", this)
Factory->>UvmFactory: ace_master::type_id::create()
UvmFactory-->>Factory: ace_master instance
Factory-->>Env: ace_master (as axi_master_base)
Env->>Factory: create_monitor("monitor", this)
Factory->>UvmFactory: ace_monitor::type_id::create()
UvmFactory-->>Factory: ace_monitor instance
Factory-->>Env: ace_monitor (as axi_monitor_base)
Note over Env,Factory: Same pattern for scoreboard, checker
Family Consistency: Compile-Time vs Runtime
Abstract Factory gives you family consistency at construction time, but where exactly does the enforcement live? There are two enforcement strategies, and the choice has real implications for how loudly your testbench fails when something goes wrong.
Runtime Enforcement (Default)
The implementation above enforces consistency at runtime. The env declares its members through the abstract base handles, and the factory's create_* methods return concrete types that satisfy the base. If you wrote a factory whose create_master() returned an ace_master and whose create_monitor() returned an axi4_monitor, the env would still build cleanly — the rule "members of one family belong together" lives only in the factory's logic.
This is fine if you trust your factory implementations. For a small set of factories that get reviewed carefully, it's enough.
Compile-Time Enforcement via Family Tags
If you want the compiler to enforce consistency, parameterize everything by a family tag.
typedef enum {AXI4_FAMILY, ACE_FAMILY, ACE_LITE_FAMILY} vip_family_e;
virtual class vip_factory_typed #(vip_family_e FAMILY) extends vip_factory_base;
pure virtual function axi_master_base#(FAMILY) create_master (string name, uvm_component parent);
pure virtual function axi_monitor_base#(FAMILY) create_monitor (string name, uvm_component parent);
pure virtual function axi_scoreboard_base#(FAMILY) create_scoreboard(string name, uvm_component parent);
endclass
class coherent_env_typed #(vip_family_e FAMILY) extends uvm_env;
vip_factory_typed#(FAMILY) factory;
axi_master_base#(FAMILY) master;
axi_monitor_base#(FAMILY) monitor;
// ... scoreboard ...
endclass
Now coherent_env_typed#(ACE_FAMILY) cannot accept an axi4_vip_factory — it's the wrong parameterization, and the compiler will reject the assignment. The downside is that every component in the family chain becomes parameterized, which adds typing noise and complicates the labels in the UVM hierarchy.
When to Use Each
| Enforcement | Pros | Cons | Use when |
|---|---|---|---|
| Runtime | Simple types, easy debugging, plays nicely with uvm_config_db | Mismatched factory implementations only fail at simulation time | Small set of carefully-reviewed factories |
| Compile-time | Compiler catches mixed families | Parameterized hierarchies everywhere, harder casts | Many third-party factories with weaker review discipline |
For most teams, runtime enforcement with a runtime sanity check is the sweet spot. Add an assertion in end_of_elaboration_phase:
function void coherent_env::end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
if (!verify_family_consistency())
`uvm_fatal("FAMILY", "VIP family inconsistency detected")
endfunction
function bit coherent_env::verify_family_consistency();
string fname = factory.family_name();
bit consistent = 1;
if (!$cast(/* probe ace_monitor */ , monitor) && fname == "ACE") consistent = 0;
// ... role-by-role checks ...
return consistent;
endfunction
Belt-and-suspenders: the factory promises consistency, and the env checks it before any traffic flows.
Pattern Combinations: Singleton, Builder, and config_db
Abstract Factory rarely lives alone. In a real testbench it pairs with three patterns from earlier in this series to form a complete configuration story.
Abstract Factory + Singleton
You typically want one factory per simulation — the same family throughout. The Singleton post showed how UVM exposes singletons through uvm_root and uvm_coreservice_t. The same pattern applies here: register the factory as a singleton resource at the top of the testbench, and every env in the hierarchy reads it from there.
class top_test extends uvm_test;
`uvm_component_utils(top_test)
vip_factory_base global_factory;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Choose the family — one place, one decision
global_factory = ace_vip_factory::type_id::create("global_factory");
// Publish for every env in the testbench
uvm_config_db#(vip_factory_base)::set(null, "*", "factory", global_factory);
super.build_phase(phase);
endfunction
endclass
uvm_config_db::set(null, "*", ...) makes the factory visible to every component in the hierarchy — the Singleton role without writing a singleton class.
Abstract Factory + Builder
The Builder post showed how to construct one complex object step by step with a fluent interface. Abstract Factory tells you what kind of object to build; Builder tells you how to configure it. They compose naturally — the env's build phase becomes a Builder that delegates each construction step to the factory.
class env_builder;
protected vip_factory_base factory;
protected coherent_env env;
function new(coherent_env env, vip_factory_base factory);
this.env = env;
this.factory = factory;
endfunction
virtual function env_builder with_master();
env.master = factory.create_master("master", env);
return this;
endfunction
virtual function env_builder with_monitor();
env.monitor = factory.create_monitor("monitor", env);
return this;
endfunction
virtual function env_builder with_full_coherency_stack();
env.scbd = factory.create_scoreboard("scbd", env);
env.checker = factory.create_checker ("checker", env);
return this;
endfunction
endclass
// Usage in env.build_phase:
env_builder builder = new(this, factory);
builder.with_master().with_monitor().with_full_coherency_stack();
Each step is family-consistent because the underlying factory enforces it. The Builder reads as English: "give me a master, a monitor, and the full coherency stack — for whichever family is configured."
Abstract Factory + uvm_config_db
The final piece is runtime family selection. You want to choose ACE vs ACE-Lite vs AXI4 from the command line, not by editing the test. The cleanest path is to read +CFG_PROTOCOL=<name> from the plusargs and instantiate the matching factory before the env builds.
class top_test extends uvm_test;
`uvm_component_utils(top_test)
function void build_phase(uvm_phase phase);
string family;
vip_factory_base factory;
super.build_phase(phase);
if (!$value$plusargs("CFG_PROTOCOL=%s", family))
family = "ACE"; // Safe default
case (family)
"AXI4": factory = axi4_vip_factory::type_id::create("factory");
"ACE": factory = ace_vip_factory::type_id::create("factory");
"ACE_LITE": factory = ace_lite_vip_factory::type_id::create("factory");
default: `uvm_fatal("CFG", $sformatf("Unknown VIP family: %s", family))
endcase
uvm_config_db#(vip_factory_base)::set(null, "*", "factory", factory);
endfunction
endclass
Run simv +CFG_PROTOCOL=ACE_LITE and the entire testbench rebuilds itself around ACE-Lite — masters, monitors, scoreboards, checkers, all of them, consistent by construction. The test source never names a concrete class. The env source never names a concrete class. Only top_test::build_phase knows the family, and even it is a one-line case statement.
This is the payoff. One factory hierarchy, one config_db handoff, and you've replaced what would otherwise be a maintenance nightmare of ifdef branches and conditional set_type_override calls.
Quick Reference
Pattern Methods Reference
| Element | Defined In | Purpose | Override? |
|---|---|---|---|
create_master() | vip_factory_base | Returns a family-specific master via UVM factory | Yes — concrete factory must implement |
create_monitor() | vip_factory_base | Returns a family-specific monitor | Yes — concrete factory must implement |
create_scoreboard() | vip_factory_base | Returns a family-specific scoreboard | Yes — concrete factory must implement |
create_checker() | vip_factory_base | Returns a family-specific coherency checker | Yes — concrete factory must implement |
family_name() | vip_factory_base | Human-readable family identifier for logs | Yes — concrete factory must implement |
uvm_config_db#(vip_factory_base)::set | UVM | Publish factory globally to all envs | No — call once in top_test |
uvm_config_db#(vip_factory_base)::get | UVM | Fetch factory inside env build_phase | No — call once in env |
Common Mistakes
| Mistake | Fix |
|---|---|
Holding env members through concrete handles (ace_master cpu_master) | Use the abstract base handle (axi_master_base cpu_master). The whole point of Abstract Factory is that the env should not know the concrete type. |
| Instance-overriding one product without the rest of the family | If you need a coverage-instrumented monitor, override the entire family — create a new concrete factory that returns the instrumented variant. Instance-overriding a single product breaks consistency. |
Forgetting to mark factory methods pure virtual | A non-pure virtual method has a default empty implementation. A concrete factory that forgets to override one will silently return null — runtime fatal, hard to debug. pure virtual makes the omission a compile error. |
| Reading the factory inside a sequence or a sequence item | Sequences and items should never depend on the factory directly. They interact with the env's components through the standard sequencer/analysis-port APIs. Only the env builds from the factory. |
Hardcoding factory selection in the env instead of top_test | The env should receive the factory through uvm_config_db. Letting the env pick its own factory defeats the purpose — you've moved the family decision back into shared code. |
Skipping the end_of_elaboration_phase consistency check | Runtime enforcement only catches mismatches when they're exercised. The phase-level check guarantees you fail before any traffic flows. |
Pattern Comparison
How does Abstract Factory relate to the other Creational patterns we've covered?
| Pattern | What It Controls | When to Reach For It |
|---|---|---|
| Factory Method | One product type | "Let tests override what gets created" — UVM's built-in factory |
| Builder | Construction steps for one object | "Building this object takes many steps and validation rules" |
| Prototype | How objects are copied | "I have a configured object and need many variants of it" |
| Singleton | How many instances exist | "There must be exactly one of these in the testbench" |
| Abstract Factory | A family of related products | "These N components must all come from the same family" |
Abstract Factory is what you reach for when family consistency is a load-bearing invariant of your testbench. AMBA protocol families are the canonical example, but the pattern shows up anywhere you have grouped concrete classes that have to match — RTL vs gate-level VIP, secure vs non-secure modes, FPGA vs ASIC build targets. Whenever you find yourself writing parallel if/else ladders that all switch on the same configuration, Abstract Factory is probably the right refactor.
Previous: Prototype Pattern — Cloning transactions, deep vs shallow copy, and do_copy()
Next: Adapter Pattern — Bridging the register model to your bus with uvm_reg_adapter
Comments (0)
Leave a Comment