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

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_base with virtual create_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 extend axi_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.

AspectFactory MethodAbstract Factory
Number of productsOne per factory methodA whole family per factory
Consistency guaranteeNone — each call is independentBuilt-in — all products come from the same family
Typical use case"Give me a transaction object""Give me a whole VIP family"
UVM analogtype_id::create()A custom factory class hierarchy
Adds value whenYou want type substitution per classYou want type substitution per family
Anti-pattern it preventsHardcoded class namesMixing 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:

  1. Abstract product base classes (one per role).
  2. Concrete products extending each base (one per family per role).
  3. An abstract factory base class with virtual create_* methods.
  4. A concrete factory per family.
  5. 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

EnforcementProsConsUse when
RuntimeSimple types, easy debugging, plays nicely with uvm_config_dbMismatched factory implementations only fail at simulation timeSmall set of carefully-reviewed factories
Compile-timeCompiler catches mixed familiesParameterized hierarchies everywhere, harder castsMany 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

ElementDefined InPurposeOverride?
create_master()vip_factory_baseReturns a family-specific master via UVM factoryYes — concrete factory must implement
create_monitor()vip_factory_baseReturns a family-specific monitorYes — concrete factory must implement
create_scoreboard()vip_factory_baseReturns a family-specific scoreboardYes — concrete factory must implement
create_checker()vip_factory_baseReturns a family-specific coherency checkerYes — concrete factory must implement
family_name()vip_factory_baseHuman-readable family identifier for logsYes — concrete factory must implement
uvm_config_db#(vip_factory_base)::setUVMPublish factory globally to all envsNo — call once in top_test
uvm_config_db#(vip_factory_base)::getUVMFetch factory inside env build_phaseNo — call once in env

Common Mistakes

MistakeFix
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 familyIf 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 virtualA 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 itemSequences 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_testThe 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 checkRuntime 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?

PatternWhat It ControlsWhen to Reach For It
Factory MethodOne product type"Let tests override what gets created" — UVM's built-in factory
BuilderConstruction steps for one object"Building this object takes many steps and validation rules"
PrototypeHow objects are copied"I have a configured object and need many variants of it"
SingletonHow many instances exist"There must be exactly one of these in the testbench"
Abstract FactoryA 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

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment