Builder Pattern in UVM: From Telescoping Constructors to Fluent Interfaces

Every AXI testbench has this pattern somewhere: a sequence that builds transactions field by field — setting the address, picking a burst type, choosing the size, configuring cache attributes, setting protection bits. For a simple read, that's five lines. For an exclusive wrapped burst with specific cache attributes? That's fifteen lines of field assignments where a single wrong value — burst_len = 4 when you meant 4 beats (which is burst_len = 3) — causes a protocol violation that won't surface until deep in simulation.

The Builder pattern separates what you want (a cacheable 4-beat write burst to 0x1000) from how it gets constructed (setting AxBURST, AxSIZE, AxLEN, AxCACHE correctly). You get readable sequences, early validation, and construction logic that lives in one place.

The Problem: Telescoping Constructors

Here's a typical AXI transaction class. Nothing exotic — just the standard fields you'd find in any AXI VIP:

A Standard AXI Transaction

typedef enum bit      { AXI_READ = 0, AXI_WRITE = 1 } axi_op_e;
typedef enum bit [1:0] { FIXED = 2'b00, INCR = 2'b01, WRAP = 2'b10 } axi_burst_e;
typedef enum bit [1:0] { OKAY = 2'b00, EXOKAY = 2'b01, SLVERR = 2'b10, DECERR = 2'b11 } axi_resp_e;

class axi_txn extends uvm_sequence_item;
  rand axi_op_e     op;
  rand bit [31:0]   addr;
  rand bit [31:0]   data[];

  rand axi_burst_e  burst_type;
  rand bit [7:0]    burst_len;    // Beats - 1
  rand bit [2:0]    burst_size;   // Bytes/beat = 2^burst_size

  rand bit [3:0]    cache;        // AxCACHE
  rand bit [2:0]    prot;         // AxPROT
  rand bit [3:0]    qos;          // AxQOS
  rand bit          lock;         // Exclusive access
  rand bit [7:0]    id;           // Transaction ID

  rand axi_resp_e   exp_resp;     // Expected response

  `uvm_object_utils(axi_txn)

  function new(string name = "axi_txn");
    super.new(name);
  endfunction
endclass

14 fields. Now build a specific transaction: a 4-beat incrementing write burst to address 0x1000 with cacheable attributes.

Approach 1: Direct Field Assignment

axi_txn txn = axi_txn::type_id::create("txn");
txn.op         = AXI_WRITE;
txn.addr       = 32'h0000_1000;
txn.data       = '{32'hAAAA, 32'hBBBB, 32'hCCCC, 32'hDDDD};
txn.burst_type = INCR;
txn.burst_len  = 3;        // 4 beats, but the field says "3"?
txn.burst_size = 3'b010;   // What's 010 — 4 bytes? 2 bytes?
txn.cache      = 4'b0011;  // Which bits are cacheable again?
txn.prot       = 3'b000;
txn.qos        = 0;
txn.lock       = 0;
txn.id         = 1;
txn.exp_resp   = OKAY;

Thirteen lines. The intent — "4-beat cacheable write to 0x1000" — is buried under protocol encoding. What does burst_len = 3 mean? Why is burst_size = 3'b010? What cache bits does 4'b0011 set? A reviewer has to decode AXI encoding to understand the test.

Approach 2: Inline Constraints

if (!txn.randomize() with {
  op         == AXI_WRITE;
  addr       == 32'h0000_1000;
  burst_type == INCR;
  burst_len  == 3;
  burst_size == 3'b010;
  cache      == 4'b0011;
  prot       == 3'b000;
  lock       == 0;
  data.size() == 4;
}) begin
  `uvm_fatal("RAND", "Randomization failed")
end

Same magic numbers, now with constraint-solver overhead for what are essentially fixed values.

Approach 3: Helper Functions

function axi_txn create_write_burst(bit [31:0] addr, int beats);
function axi_txn create_read_burst(bit [31:0] addr, int beats);
function axi_txn create_exclusive_write(bit [31:0] addr, bit [31:0] data);
function axi_txn create_cacheable_write_burst(bit [31:0] addr, int beats, bit [3:0] cache);
function axi_txn create_exclusive_cacheable_wrapped_write(...);

This is the Telescoping Constructor anti-pattern, named by Joshua Bloch in Effective Java. Each combination of options demands a new function. With N optional features, you need up to 2N helpers. Burst type × cache policy × lock mode × protection = combinatorial explosion.

All three approaches share the same root cause: construction knowledge is scattered. The rules of AXI — how burst_len relates to beat count, which cache bits mean what, when lock requires specific alignment — live nowhere specific. They're repeated in every sequence, every test, every review comment that says "this burst_len looks wrong."

Now compare:

axi_txn txn = axi_builder::create()
  .write(32'h1000)
  .data('{32'hAAAA, 32'hBBBB, 32'hCCCC, 32'hDDDD})
  .incr_burst(4)
  .cacheable()
  .build();

Five lines. No magic numbers. The intent is the code.

Gang of Four: Builder Patterns

The Gang of Four defines the Builder pattern as:

Separate the construction of a complex object from its representation so that the same construction process can create different representations.

The pattern has three forms. Each solves a different aspect of the construction problem.

1. Classic Builder (GoF)

The original pattern separates what to build (Director) from how to build it (Builder):

classDiagram
    class Director {
        -builder : Builder
        +construct()
    }
    class Builder {
        <<abstract>>
        +setAddress()*
        +setBurst()*
        +setCache()*
        +getResult()* : Product
    }
    class ConcreteBuilder {
        -product : Product
        +setAddress()
        +setBurst()
        +setCache()
        +getResult() : Product
    }
    class Product

    Director o-- Builder
    Builder <|-- ConcreteBuilder
    ConcreteBuilder ..> Product : creates

The Director defines construction order. The Builder defines construction mechanics. Neither knows about the other's internals.

2. Fluent Builder (Bloch Variant)

Joshua Bloch adapted the pattern in Effective Java for objects with many optional parameters. Every setter returns this, enabling method chaining:

classDiagram
    class AxiBuilder {
        -addr : bit[31:0]
        -burst_type : axi_burst_e
        -cache : bit[3:0]
        +create()$ : AxiBuilder
        +write(addr) : AxiBuilder
        +incr_burst(beats) : AxiBuilder
        +cacheable() : AxiBuilder
        +build() : axi_txn
    }
    class axi_txn {
        +op
        +addr
        +burst_type
        +cache
    }

    AxiBuilder ..> axi_txn : creates

Martin Fowler calls this a fluent interface — code that reads like a declaration:

txn = axi_builder::create().write(32'h1000).incr_burst(4).cacheable().build();

This is the form you'll use most in testbenches. The Director form becomes useful when you need reusable construction recipes — we'll cover that in the advanced section.

3. Director Pattern

A Director encapsulates a construction sequence. Different Directors use the same Builder to produce different results:

sequenceDiagram
    participant Test
    participant Director
    participant Builder
    participant Txn as axi_txn

    Test->>Director: construct()
    Director->>Builder: write(addr)
    Director->>Builder: incr_burst(4)
    Director->>Builder: cacheable()
    Director->>Builder: build()
    Builder-->>Director: Txn
    Director-->>Test: Txn

This matters at scale: a memory_test_director and an exclusive_access_director both use the same axi_builder but produce fundamentally different transaction sequences.

SOLID Principles

The Builder pattern aligns with four SOLID principles — this is why it scales in verification environments:

PrincipleHow Builder Applies
Single Responsibilityaxi_txn holds data. The Builder handles construction. Neither does the other's job.
Open-ClosedNew transaction variants = new Builder chains or Directors. No modifications to existing classes.
Liskov SubstitutionAny Director works with any Builder that implements the interface.
Dependency InversionSequences depend on the Builder abstraction, not on AXI field encoding.

AXI Transaction Builder

Here's a complete axi_builder that produces axi_txn sequence items. Every setter returns this for chaining. The build() method validates AXI protocol rules before creating the product.

Builder Skeleton

class axi_builder;

  // Internal state
  protected axi_op_e     m_op;
  protected bit [31:0]   m_addr;
  protected bit [31:0]   m_data[$];
  protected axi_burst_e  m_burst_type;
  protected int unsigned  m_beats;
  protected bit [2:0]    m_burst_size;
  protected bit [3:0]    m_cache;
  protected bit [2:0]    m_prot;
  protected bit [3:0]    m_qos;
  protected bit          m_lock;
  protected bit [7:0]    m_id;
  protected axi_resp_e   m_exp_resp;

  // Private constructor with sensible defaults
  local function new();
    m_op         = AXI_READ;
    m_burst_type = INCR;
    m_beats      = 1;
    m_burst_size = 3'b010;  // 4 bytes (32-bit bus)
    m_exp_resp   = OKAY;
  endfunction

  // Entry point
  static function axi_builder create();
    axi_builder b = new();
    return b;
  endfunction

The local constructor prevents external new() calls. All construction goes through create() — this lets you add builder pooling or pre-configured templates later without breaking callers.

Notice m_beats instead of m_burst_len. Internally, the Builder thinks in beats (human-readable: "4 beats") and converts to AXI encoding (burst_len = 3) in apply(). This is the Builder's core value: encode domain knowledge once, use it everywhere.

Fluent Setters

Setters come in two levels: field-level for full control, and semantic for readability.

Operation and Address

  // --- Operation ---
  function axi_builder read(bit [31:0] addr);
    m_op   = AXI_READ;
    m_addr = addr;
    return this;
  endfunction

  function axi_builder write(bit [31:0] addr);
    m_op   = AXI_WRITE;
    m_addr = addr;
    return this;
  endfunction

  function axi_builder data(bit [31:0] d[$]);
    m_data = d;
    return this;
  endfunction

Burst Configuration

  // --- Burst: Semantic setters ---
  // User says "4 beats", Builder handles burst_len = 3
  function axi_builder incr_burst(int unsigned beats);
    m_burst_type = INCR;
    m_beats      = beats;
    return this;
  endfunction

  function axi_builder wrap_burst(int unsigned beats);
    m_burst_type = WRAP;
    m_beats      = beats;
    return this;
  endfunction

  function axi_builder fixed_burst(int unsigned beats);
    m_burst_type = FIXED;
    m_beats      = beats;
    return this;
  endfunction

  // Single-beat transfer (default)
  function axi_builder single();
    m_burst_type = INCR;
    m_beats      = 1;
    return this;
  endfunction

  // Field-level: override bytes per beat
  function axi_builder with_size(int unsigned bytes_per_beat);
    case (bytes_per_beat)
      1:   m_burst_size = 3'b000;
      2:   m_burst_size = 3'b001;
      4:   m_burst_size = 3'b010;
      8:   m_burst_size = 3'b011;
      16:  m_burst_size = 3'b100;
      32:  m_burst_size = 3'b101;
      64:  m_burst_size = 3'b110;
      128: m_burst_size = 3'b111;
    endcase
    return this;
  endfunction

The burst setters hide AXI's burst_len = beats - 1 encoding. No more off-by-one bugs. incr_burst(4) means 4 beats — the Builder converts to burst_len = 3 internally.

Cache, Protection, and QoS

  // --- Cache: Semantic presets ---
  // AxCACHE[0]=Bufferable, [1]=Cacheable, [2]=Read-Alloc, [3]=Write-Alloc
  function axi_builder bufferable();
    m_cache[0] = 1;
    return this;
  endfunction

  function axi_builder cacheable();
    m_cache[1] = 1;
    m_cache[0] = 1;  // Cacheable implies bufferable
    return this;
  endfunction

  function axi_builder write_back();
    m_cache = 4'b1111;  // Full caching: WA + RA + C + B
    return this;
  endfunction

  // Field-level
  function axi_builder with_cache(bit [3:0] cache);
    m_cache = cache;
    return this;
  endfunction

  // --- Protection ---
  // AxPROT[0]=Privileged, [1]=Non-secure, [2]=Instruction
  function axi_builder privileged();
    m_prot[0] = 1;
    return this;
  endfunction

  function axi_builder non_secure();
    m_prot[1] = 1;
    return this;
  endfunction

  function axi_builder instruction();
    m_prot[2] = 1;
    return this;
  endfunction

  // Field-level
  function axi_builder with_prot(bit [2:0] prot);
    m_prot = prot;
    return this;
  endfunction

  // --- Other ---
  function axi_builder exclusive();
    m_lock = 1;
    return this;
  endfunction

  function axi_builder with_qos(bit [3:0] qos);
    m_qos = qos;
    return this;
  endfunction

  function axi_builder with_id(bit [7:0] id);
    m_id = id;
    return this;
  endfunction

  function axi_builder expect_slverr();
    m_exp_resp = SLVERR;
    return this;
  endfunction

  function axi_builder expect_decerr();
    m_exp_resp = DECERR;
    return this;
  endfunction

Two levels of abstraction. cacheable() and privileged() encode AXI semantics — no one needs to remember that AxCACHE[1] is the cacheable bit. with_cache() and with_prot() give full control when you need to test specific bit patterns.

Validation and Build

The build() method is the only way to get a product. It validates AXI protocol rules at construction time — catching errors before simulation, not 100K cycles in when a scoreboard mismatch surfaces.

  // --- Terminal Operation ---
  function axi_txn build();
    validate();
    return apply();
  endfunction

  protected function void validate();
    // WRAP burst: length must be 2, 4, 8, or 16
    if (m_burst_type == WRAP) begin
      if (!(m_beats inside {2, 4, 8, 16})) begin
        `uvm_fatal("AXI_BUILD", $sformatf(
          "WRAP burst length must be 2, 4, 8, or 16. Got %0d.", m_beats))
      end
    end

    // INCR burst: cannot cross 4KB boundary
    if (m_burst_type == INCR && m_beats > 1) begin
      int unsigned bytes_per_beat = 1 << m_burst_size;
      int unsigned total_bytes    = m_beats * bytes_per_beat;
      int unsigned start_4kb      = m_addr[31:12];
      int unsigned end_4kb        = (m_addr + total_bytes - 1) >> 12;
      if (start_4kb != end_4kb) begin
        `uvm_warning("AXI_BUILD", $sformatf(
          "INCR burst [0x%08h + %0d bytes] crosses 4KB boundary.",
          m_addr, total_bytes))
      end
    end

    // Write data must match beat count
    if (m_op == AXI_WRITE && m_data.size() > 0 && m_data.size() != m_beats) begin
      `uvm_fatal("AXI_BUILD", $sformatf(
        "Write data size (%0d) doesn't match beat count (%0d).",
        m_data.size(), m_beats))
    end

    // Exclusive requires aligned address
    if (m_lock) begin
      int unsigned align = (1 << m_burst_size) * m_beats;
      if (m_addr % align != 0) begin
        `uvm_warning("AXI_BUILD", $sformatf(
          "Exclusive access at 0x%08h is not %0d-byte aligned.",
          m_addr, align))
      end
    end
  endfunction

  protected function axi_txn apply();
    axi_txn t = axi_txn::type_id::create("txn");
    t.op         = m_op;
    t.addr       = m_addr;
    t.burst_type = m_burst_type;
    t.burst_len  = m_beats - 1;  // AXI encoding: beats - 1
    t.burst_size = m_burst_size;
    t.cache      = m_cache;
    t.prot       = m_prot;
    t.qos        = m_qos;
    t.lock       = m_lock;
    t.id         = m_id;
    t.exp_resp   = m_exp_resp;

    if (m_op == AXI_WRITE && m_data.size() > 0) begin
      t.data = new[m_data.size()];
      foreach (m_data[i]) begin
        t.data[i] = m_data[i];
      end
    end
    else if (m_op == AXI_WRITE) begin
      t.data = new[m_beats];
    end

    return t;
  endfunction

endclass

validate() catches three classes of errors:

  • Protocol violations — WRAP burst with invalid length
  • Boundary issues — INCR burst crossing 4KB (AXI spec requires this not to happen)
  • Consistency errors — Write data array size doesn't match burst length

apply() handles the translation from human-friendly values to AXI encoding. The critical line: t.burst_len = m_beats - 1. This off-by-one conversion lives in one place, not scattered across every sequence.

Note that build() creates the transaction via axi_txn::type_id::create() — the UVM factory. If a test overrides axi_txn with axi_txn_with_coverage, the Builder automatically produces the overridden type. Builder and Factory work together.

Usage in Sequences

With the Builder, sequences express test intent:

class axi_directed_seq extends uvm_sequence #(axi_txn);
  `uvm_object_utils(axi_directed_seq)

  function new(string name = "axi_directed_seq");
    super.new(name);
  endfunction

  task body();
    axi_txn txn;

    // 4-beat cacheable write to 0x1000
    txn = axi_builder::create()
      .write(32'h0000_1000)
      .data('{32'hAAAA, 32'hBBBB, 32'hCCCC, 32'hDDDD})
      .incr_burst(4)
      .cacheable()
      .with_id(1)
      .build();
    `uvm_send(txn)

    // Read back the same region
    txn = axi_builder::create()
      .read(32'h0000_1000)
      .incr_burst(4)
      .cacheable()
      .with_id(2)
      .build();
    `uvm_send(txn)

    // Exclusive write — atomic operation
    txn = axi_builder::create()
      .write(32'h0000_2000)
      .data('{32'hDEAD_BEEF})
      .exclusive()
      .build();
    `uvm_send(txn)

    // Wrapped burst to device register space
    txn = axi_builder::create()
      .read(32'h4000_0000)
      .wrap_burst(4)
      .non_secure()
      .expect_slverr()
      .build();
    `uvm_send(txn)
  endtask
endclass

Each transaction reads as a statement of intent. A reviewer understands the test plan — write, read-back, exclusive atomic, device access with expected error — without decoding AXI field encodings.

Builder + Randomization

The Builder doesn't replace constrained random — it complements it. Use the Builder to set a transaction's structural identity, then let the constraint solver vary the rest:

class axi_random_burst_seq extends uvm_sequence #(axi_txn);
  `uvm_object_utils(axi_random_burst_seq)

  function new(string name = "axi_random_burst_seq");
    super.new(name);
  endfunction

  task body();
    axi_txn txn;

    repeat (100) begin
      // Builder: deterministic structure
      txn = axi_builder::create()
        .write(32'h0000_0000)
        .incr_burst(4)
        .cacheable()
        .build();

      // Randomize: vary address and data within bounds
      if (!txn.randomize() with {
        addr inside {[32'h0000_0000 : 32'h0000_FFFF]};
        foreach (data[i]) data[i] != 0;
      }) begin
        `uvm_fatal("RAND", "Randomization failed")
      end

      `uvm_send(txn)
    end
  endtask
endclass

The Builder provides the deterministic skeleton — "this is a 4-beat cacheable INCR write." Randomization provides variation within bounds — different addresses, different data patterns. You get both test readability and coverage diversity.

Builder vs Alternatives

The Builder isn't always the right tool. Use the simplest approach that works:

ApproachBest ForBreaks Down When
randomize() with {}Pure random, few constraintsConstraint blocks exceed ~10 lines; cross-field rules need validation
Field assignmentQuick debug, 3-4 fieldsMagic numbers accumulate; protocol rules aren't enforced
Helper functions2-3 fixed transaction typesCombinations multiply past ~5 functions
uvm_config_dbEnvironment-level configPer-transaction construction — wrong granularity
BuilderComplex objects, optional fields, protocol rulesSimple objects with <5 fields — overhead not justified

The same transaction, four ways:

// 1. Field assignment — magic numbers everywhere
txn.op = AXI_WRITE; txn.addr = 32'h1000;
txn.burst_type = INCR; txn.burst_len = 3;
txn.burst_size = 3'b010; txn.cache = 4'b0011;

// 2. Inline constraints — solver overhead for constants
txn.randomize() with {
  op == AXI_WRITE; addr == 32'h1000;
  burst_type == INCR; burst_len == 3;
  burst_size == 3'b010; cache == 4'b0011;
};

// 3. Helper — what's the 5th argument?
txn = create_write_burst(32'h1000, 4, 3'b010, 4'b0011, 0);

// 4. Builder — self-documenting
txn = axi_builder::create()
  .write(32'h1000)
  .incr_burst(4)
  .cacheable()
  .build();

Advanced: Directors for Test Scenarios

The fluent Builder is great for individual transactions. But verification operates at the scenario level — sequences of transactions that exercise specific DUT behavior. The Director pattern wraps reusable construction recipes around the Builder.

Base Director

virtual class axi_director;

  // Template Method: subclasses fill in the steps
  pure virtual function void get_txns(ref axi_txn txns[$]);

endclass

Memory Test Director

Generates write-then-read pairs across an address range — the bread-and-butter of memory subsystem verification:

class memory_test_director extends axi_director;

  protected bit [31:0]    m_base_addr;
  protected int unsigned  m_region_size;
  protected int unsigned  m_burst_beats;

  function new(bit [31:0] base_addr, int unsigned region_size = 256,
    int unsigned burst_beats = 4);
    m_base_addr   = base_addr;
    m_region_size = region_size;
    m_burst_beats = burst_beats;
  endfunction

  virtual function void get_txns(ref axi_txn txns[$]);
    int unsigned stride = m_burst_beats * 4;

    for (int unsigned offset = 0; offset < m_region_size; offset += stride) begin
      // Write burst
      txns.push_back(
        axi_builder::create()
          .write(m_base_addr + offset)
          .incr_burst(m_burst_beats)
          .cacheable()
          .with_id(offset[7:0])
          .build()
      );

      // Read-back
      txns.push_back(
        axi_builder::create()
          .read(m_base_addr + offset)
          .incr_burst(m_burst_beats)
          .cacheable()
          .with_id(offset[7:0])
          .build()
      );
    end
  endfunction

endclass

Exclusive Access Director

Generates exclusive read → modify → exclusive write pairs for atomic operation testing:

class exclusive_access_director extends axi_director;

  protected bit [31:0] m_addrs[$];

  function new(bit [31:0] addrs[$]);
    m_addrs = addrs;
  endfunction

  virtual function void get_txns(ref axi_txn txns[$]);
    foreach (m_addrs[i]) begin
      // Exclusive read (load-linked)
      txns.push_back(
        axi_builder::create()
          .read(m_addrs[i])
          .exclusive()
          .with_id(i[7:0])
          .build()
      );

      // Exclusive write (store-conditional)
      txns.push_back(
        axi_builder::create()
          .write(m_addrs[i])
          .data('{32'hFFFF_FFFF})
          .exclusive()
          .with_id(i[7:0])
          .build()
      );
    end
  endfunction

endclass

Composing Directors in a Virtual Sequence

class axi_regression_vseq extends uvm_sequence #(axi_txn);
  `uvm_object_utils(axi_regression_vseq)

  function new(string name = "axi_regression_vseq");
    super.new(name);
  endfunction

  task body();
    axi_txn txns[$];

    // Phase 1: Memory sweep
    begin
      memory_test_director dir = new(
        .base_addr(32'h0000_0000),
        .region_size(1024),
        .burst_beats(8)
      );
      dir.get_txns(txns);
      send_all(txns);
      txns.delete();
    end

    // Phase 2: Atomic operations
    begin
      exclusive_access_director dir = new(
        '{32'h0001_0000, 32'h0001_0010, 32'h0001_0020}
      );
      dir.get_txns(txns);
      send_all(txns);
    end
  endtask

  task send_all(ref axi_txn txns[$]);
    foreach (txns[i]) begin
      start_item(txns[i]);
      finish_item(txns[i]);
    end
  endtask
endclass

The virtual sequence reads like a test plan: memory sweep, then atomic operations. Directors are reusable across tests. New scenarios = new Directors, not new copy-pasted sequences.

Quick Reference

OperationCode
Create builderaxi_builder::create()
Read/Write.read(addr), .write(addr)
Burst.incr_burst(beats), .wrap_burst(beats), .fixed_burst(beats)
Cache.cacheable(), .bufferable(), .write_back(), .with_cache(val)
Protection.privileged(), .non_secure(), .instruction(), .with_prot(val)
Exclusive.exclusive()
Expected response.expect_slverr(), .expect_decerr()
Build.build()

When to Use Builder

SituationUse
Simple read/write, few fieldsrandomize() with {} or field assignment
Fixed configs used everywhereHelper functions (2-3 max)
Complex transactions with protocol rulesBuilder
Reusable multi-transaction scenariosBuilder + Director

Common Mistakes

MistakeFix
Using Builder for 3-field transactionsDirect assignment is fine for simple cases
Forgetting .build()Builder is not the product — always call .build()
Skipping validation in build()Validation is Builder's main advantage. Always validate.
Setting burst_len directlyUse .incr_burst(beats) — let the Builder handle encoding
Director that hard-codes protocol fieldsKeep AXI encoding in the Builder, keep scenario logic in the Director

Builder Template

Copy this as a starting point for any protocol:

class my_txn_builder;
  protected bit [31:0] m_addr;
  protected bit [31:0] m_data;

  local function new();
  endfunction

  static function my_txn_builder create();
    my_txn_builder b = new();
    return b;
  endfunction

  function my_txn_builder with_addr(bit [31:0] addr);
    m_addr = addr;
    return this;
  endfunction

  function my_txn build();
    validate();
    my_txn t = my_txn::type_id::create("t");
    t.addr = m_addr;
    t.data = m_data;
    return t;
  endfunction

  protected function void validate();
    // Protocol rules here
  endfunction
endclass

Previous: Singleton Pattern — uvm_root, uvm_config_db, and global resources

Next: Prototype Pattern — clone() methods, transaction copying, and do_copy()

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

Comments (0)

Leave a Comment