Creational Patterns: Factory and Builder in Verification

Every UVM engineer uses the factory. Few understand it. Even fewer realize there's a whole family of creational patterns - Factory Method, Abstract Factory, Builder - each solving different object creation problems. This post goes beyond type_id::create() to show you what these patterns really do and when to apply them.

The Problem: Why Object Creation Matters

Consider this seemingly innocent code:

class axi_scoreboard extends uvm_scoreboard;
  
  function void check_transaction(axi_transaction t);
    // Need a reference transaction for comparison
    axi_transaction ref_txn = new("ref_txn");  // Direct instantiation
    ref_txn.copy(expected_model.predict(t));
    // ...
  endfunction
  
endclass

What's wrong with new()? Several things:

  • Tight coupling: The scoreboard is hardwired to axi_transaction. Want to use axi4_transaction or axi_lite_transaction? Modify the scoreboard.
  • No extensibility: A test can't substitute a specialized transaction type.
  • Violates Open/Closed: Adding new transaction types requires changing existing code.

The Gang of Four identified this in 1994:

"Program to an interface, not an implementation... Object creation often involves more than simply instantiating a class."

Design Patterns, Gamma et al.

Factory Method: Delegation of Creation

The Factory Method pattern delegates object creation to a method that subclasses can override. It's the simplest creational pattern and appears throughout UVM.

classDiagram
    class Creator {
        +factoryMethod() Product
        +operation()
    }
    class ConcreteCreator {
        +factoryMethod() ConcreteProduct
    }
    class Product {
        <<interface>>
    }
    class ConcreteProduct
    
    Creator <|-- ConcreteCreator
    Product <|.. ConcreteProduct
    Creator ..> Product : creates
    ConcreteCreator ..> ConcreteProduct : creates

Factory Method in UVM Sequences

You've been using Factory Method without knowing it:

class base_sequence extends uvm_sequence #(axi_transaction);
  
  // Factory method - subclasses override to create different transaction types
  virtual function axi_transaction create_transaction(string name);
    axi_transaction t = axi_transaction::type_id::create(name);
    return t;
  endfunction
  
  task body();
    axi_transaction req;
    
    repeat(num_transactions) begin
      req = create_transaction("req");  // Calls factory method
      start_item(req);
      if (!req.randomize()) `uvm_fatal("SEQ", "Randomization failed")
      finish_item(req);
    end
  endtask
  
endclass

// Subclass overrides factory method
class error_injection_sequence extends base_sequence;
  
  virtual function axi_transaction create_transaction(string name);
    // Create error-capable transaction instead
    axi_error_transaction t = axi_error_transaction::type_id::create(name);
    return t;  // Polymorphism: returns base type
  endfunction
  
endclass

The base sequence doesn't know (or care) what concrete type it's creating. The factory method provides a seam - a point where behavior can change without modifying the caller.

UVM Factory: Abstract Factory on Steroids

The UVM Factory is an implementation of the Abstract Factory pattern combined with a Registry. Understanding this reveals its full power.

What type_id::create() Actually Does

// This innocent line...
axi_transaction t = axi_transaction::type_id::create("t", this);

// ...is shorthand for approximately:
axi_transaction t;
uvm_factory factory = uvm_factory::get();  // Singleton
uvm_object_wrapper wrapper = factory.find_override_by_type(
    axi_transaction::get_type(), get_full_name());
t = wrapper.create_object("t");
if (!$cast(t, obj)) `uvm_fatal(...)

The factory:

  1. Looks up whether any override exists for this type at this path
  2. Uses the (possibly overridden) type's wrapper to create the object
  3. Returns it as the requested type

Override Mechanisms

UVM provides two override dimensions:

Override TypeScopeUse Case
set_type_override_by_typeGlobalReplace all instances of a type
set_inst_override_by_typePath-specificReplace only at specific hierarchy
class error_test extends base_test;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    // Global: All axi_transaction become axi_error_transaction
    axi_transaction::type_id::set_type_override(
        axi_error_transaction::get_type());
    
    // Instance-specific: Only agent1's driver gets error transactions
    axi_transaction::type_id::set_inst_override(
        axi_error_transaction::get_type(),
        "env.agent1.driver.*");
  endfunction
  
endclass

The Registration Mechanism

Ever wonder what `uvm_object_utils actually does?

// This macro...
`uvm_object_utils(axi_transaction)

// ...expands to approximately:
typedef uvm_object_registry #(axi_transaction, "axi_transaction") type_id;

static function type_id get_type();
  return type_id::get();
endfunction

function uvm_object_wrapper get_object_type();
  return type_id::get();
endfunction

The uvm_object_registry is a parameterized singleton that:

  1. Registers the type with the global factory at elaboration time
  2. Provides a wrapper that knows how to create instances
  3. Enables string-based and type-based lookup

Factory Power: Configuration-Driven Testbenches

// In your test, driven by a configuration file or plusarg:
function void configure_factory_from_plusargs();
  string override_type;
  
  if ($value$plusargs("TXN_TYPE=%s", override_type)) begin
    case (override_type)
      "error":   axi_transaction::type_id::set_type_override(
                     axi_error_transaction::get_type());
      "slow":    axi_transaction::type_id::set_type_override(
                     axi_slow_transaction::get_type());
      "minimal": axi_transaction::type_id::set_type_override(
                     axi_minimal_transaction::get_type());
    endcase
  end
endfunction

Now the same testbench supports multiple transaction types without recompilation.

Builder Pattern: Complex Object Construction

Factory patterns decide which class to instantiate. The Builder pattern addresses a different problem: how to construct complex objects step by step.

The Problem: Telescoping Constructors

// Anti-pattern: Too many constructor parameters
class pcie_tlp extends uvm_sequence_item;
  
  function new(string name = "pcie_tlp",
               tlp_type_e tlp_type = MEM_RD,
               bit [63:0] address = 0,
               bit [9:0] length = 1,
               bit [7:0] tag = 0,
               bit [15:0] requester_id = 0,
               traffic_class_e tc = TC0,
               bit poisoned = 0,
               bit [3:0] first_be = 4'hF,
               bit [3:0] last_be = 4'hF,
               // ... 10 more parameters
              );
    // ...
  endfunction
  
endclass

// Calling code is unreadable
pcie_tlp tlp = new("tlp", MEM_WR, 64'h1000, 10'd4, 8'd1, 16'h100, 
                    TC0, 0, 4'hF, 4'hF, ...);

Which parameter is which? What if you want defaults for most but need to set one in the middle?

Builder Solution: Fluent Interface

class pcie_tlp_builder;
  
  // Internal state
  protected tlp_type_e   m_type;
  protected bit [63:0]   m_address;
  protected bit [9:0]    m_length;
  protected bit [7:0]    m_tag;
  protected bit [15:0]   m_requester_id;
  protected traffic_class_e m_tc;
  protected bit          m_poisoned;
  protected bit [3:0]    m_first_be;
  protected bit [3:0]    m_last_be;
  protected bit [31:0]   m_data[$];
  
  // Fluent setters - each returns 'this' for chaining
  function pcie_tlp_builder set_type(tlp_type_e t);
    m_type = t;
    return this;
  endfunction
  
  function pcie_tlp_builder set_address(bit [63:0] addr);
    m_address = addr;
    return this;
  endfunction
  
  function pcie_tlp_builder set_length(bit [9:0] len);
    m_length = len;
    return this;
  endfunction
  
  function pcie_tlp_builder set_tag(bit [7:0] tag);
    m_tag = tag;
    return this;
  endfunction
  
  function pcie_tlp_builder set_traffic_class(traffic_class_e tc);
    m_tc = tc;
    return this;
  endfunction
  
  function pcie_tlp_builder set_poisoned(bit p = 1);
    m_poisoned = p;
    return this;
  endfunction
  
  function pcie_tlp_builder add_data(bit [31:0] dw);
    m_data.push_back(dw);
    return this;
  endfunction
  
  function pcie_tlp_builder set_data(bit [31:0] data[$]);
    m_data = data;
    return this;
  endfunction
  
  // Terminal operation: build the object
  function pcie_tlp build();
    pcie_tlp tlp = pcie_tlp::type_id::create("tlp");
    
    tlp.tlp_type = m_type;
    tlp.address = m_address;
    tlp.length = m_length;
    tlp.tag = m_tag;
    tlp.requester_id = m_requester_id;
    tlp.tc = m_tc;
    tlp.poisoned = m_poisoned;
    tlp.first_be = m_first_be;
    tlp.last_be = m_last_be;
    tlp.data = m_data;
    
    // Validation
    if (m_type inside {MEM_WR, CPL_D} && m_data.size() == 0)
      `uvm_warning("BUILDER", "Write/CplD TLP with no data")
    
    return tlp;
  endfunction
  
  // Reset for reuse
  function void reset();
    m_type = MEM_RD;
    m_address = 0;
    m_length = 1;
    m_tag = 0;
    m_requester_id = 0;
    m_tc = TC0;
    m_poisoned = 0;
    m_first_be = 4'hF;
    m_last_be = 4'hF;
    m_data.delete();
  endfunction
  
  function new();
    reset();
  endfunction
  
endclass

Using the Builder

class pcie_sequence extends uvm_sequence #(pcie_tlp);
  
  pcie_tlp_builder builder;
  
  function new(string name = "pcie_sequence");
    super.new(name);
    builder = new();
  endfunction
  
  task body();
    pcie_tlp tlp;
    
    // Readable, self-documenting construction
    tlp = builder
        .set_type(MEM_WR)
        .set_address(64'h0000_1000)
        .set_length(4)
        .set_tag(8'h01)
        .set_traffic_class(TC2)
        .add_data(32'hDEAD_BEEF)
        .add_data(32'hCAFE_BABE)
        .add_data(32'h1234_5678)
        .add_data(32'h9ABC_DEF0)
        .build();
    
    start_item(tlp);
    finish_item(tlp);
    
    builder.reset();  // Reuse builder
    
    // Another TLP - only set what differs from defaults
    tlp = builder
        .set_type(MEM_RD)
        .set_address(64'h0000_2000)
        .set_length(1)
        .build();
    
    start_item(tlp);
    finish_item(tlp);
  endtask
  
endclass

Builder + Randomization

Combine Builder with constrained random for the best of both worlds:

class pcie_tlp_builder;
  
  // ... previous code ...
  
  // Build with constraints
  function pcie_tlp build_randomized(int max_length = 128);
    pcie_tlp tlp = pcie_tlp::type_id::create("tlp");
    
    // Apply builder settings as constraints
    if (!tlp.randomize() with {
      tlp_type == local::m_type;
      address == local::m_address;
      length inside {[1:max_length]};
      tc == local::m_tc;
      // Let other fields randomize freely
    }) `uvm_fatal("BUILDER", "Randomization failed")
    
    return tlp;
  endfunction
  
endclass

// Usage: Constrain what matters, randomize the rest
tlp = builder
    .set_type(MEM_WR)
    .set_address(64'h1000)
    .build_randomized(16);  // Random length 1-16, random data

Director Pattern: Reusable Build Sequences

When you find yourself building the same object configurations repeatedly, extract them into a Director:

class pcie_tlp_director;
  
  pcie_tlp_builder builder;
  
  function new();
    builder = new();
  endfunction
  
  // Predefined configurations
  function pcie_tlp build_config_read(bit [11:0] reg_addr);
    return builder
        .set_type(CFG_RD0)
        .set_address({52'b0, reg_addr})
        .set_length(1)
        .build();
  endfunction
  
  function pcie_tlp build_config_write(bit [11:0] reg_addr, bit [31:0] data);
    return builder
        .set_type(CFG_WR0)
        .set_address({52'b0, reg_addr})
        .set_length(1)
        .add_data(data)
        .build();
  endfunction
  
  function pcie_tlp build_msi_write(bit [63:0] msi_addr, bit [31:0] msi_data);
    return builder
        .set_type(MEM_WR)
        .set_address(msi_addr)
        .set_length(1)
        .set_traffic_class(TC0)  // MSI always TC0
        .add_data(msi_data)
        .build();
  endfunction
  
  function pcie_tlp build_dma_read(bit [63:0] host_addr, int num_dwords);
    return builder
        .set_type(MEM_RD)
        .set_address(host_addr)
        .set_length(num_dwords)
        .build();
  endfunction
  
endclass

// Clean, intention-revealing code
pcie_tlp_director director = new();

pcie_tlp cfg_rd  = director.build_config_read(12'h010);  // Read Device ID
pcie_tlp msi     = director.build_msi_write(msi_addr, msi_data);
pcie_tlp dma_req = director.build_dma_read(buffer_addr, 64);

When to Use Which Pattern

flowchart TD
    A[Need to create objects?] --> B{One type or family?}
    B -->|One type| C{Complex construction?}
    B -->|Family of types| D[Abstract Factory]
    C -->|Simple| E{Need runtime substitution?}
    C -->|Complex, multi-step| F[Builder]
    E -->|Yes| G[Factory Method]
    E -->|No| H[Direct new]
    
    style D fill:#d1fae5,stroke:#10b981
    style F fill:#d1fae5,stroke:#10b981
    style G fill:#d1fae5,stroke:#10b981
    style H fill:#fef3c7,stroke:#f59e0b
PatternProblemUVM Example
Factory MethodLet subclasses decide which class to instantiateVirtual create_transaction() in base sequence
Abstract Factory (UVM Factory)Create families of related objectstype_id::create() with overrides
BuilderConstruct complex objects step-by-stepTLP builder with fluent interface
DirectorReusable build configurationsCommon TLP configurations

Common Mistakes

1. Using new() When Factory is Available

// BAD: Bypasses factory, breaks overrides
axi_transaction t = new("t");

// GOOD: Uses factory, enables test customization
axi_transaction t = axi_transaction::type_id::create("t", this);

2. Forgetting to Register Types

// BAD: No registration - factory can't create or override
class my_transaction extends uvm_sequence_item;
  // Missing `uvm_object_utils(my_transaction)
endclass

// GOOD: Properly registered
class my_transaction extends uvm_sequence_item;
  `uvm_object_utils(my_transaction)
  // ...
endclass

3. Override Order Matters

// BAD: Override after object creation
function void build_phase(uvm_phase phase);
  env = my_env::type_id::create("env", this);  // Created first
  
  // Too late! env already created with original type
  my_env::type_id::set_type_override(custom_env::get_type());
endfunction

// GOOD: Override before creation
function void build_phase(uvm_phase phase);
  my_env::type_id::set_type_override(custom_env::get_type());
  env = my_env::type_id::create("env", this);  // Gets override
endfunction

4. Builder Without Reset

// BAD: Builder state accumulates
tlp1 = builder.set_type(MEM_WR).set_address(64'h1000).build();
tlp2 = builder.set_type(MEM_RD).build();  // Still has address 0x1000!

// GOOD: Reset between uses
tlp1 = builder.set_type(MEM_WR).set_address(64'h1000).build();
builder.reset();
tlp2 = builder.set_type(MEM_RD).set_address(64'h2000).build();

Key Takeaways

  • Factory Method: Delegate object creation to overridable methods
  • UVM Factory: Global Abstract Factory + Registry enabling test-time type substitution
  • Always use type_id::create(): Direct new() breaks the override mechanism
  • Builder Pattern: Construct complex objects with readable, step-by-step fluent interface
  • Director: Encapsulate common build configurations for reuse
  • Combine patterns: Builder + Factory + Randomization for maximum flexibility

Further Reading

  • Design Patterns, Chapter 3 (Creational Patterns) - Gamma, Helm, Johnson, Vlissides
  • Effective Java, Item 2 (Builder Pattern) - Joshua Bloch
  • Head First Design Patterns, Chapter 4 (Factory Pattern) - Freeman & Robson
  • UVM 1.2 Class Reference - uvm_factory, uvm_object_registry

Interview Corner

Q: Why should you use type_id::create() instead of new() in UVM?

A: type_id::create() uses the UVM factory, which enables type overrides. Tests can substitute derived types without modifying the original code. Direct new() bypasses this mechanism.

Q: What's the difference between Factory Method and Abstract Factory?

A: Factory Method uses inheritance - a method creates one product, subclasses override to change the product type. Abstract Factory uses composition - an object creates families of related products, and the entire factory object can be substituted.


Previous: Part 0 - Introduction | Next: Structural Patterns

Return to Software Engineering for DV

Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment