Skip to content

UVM

Why Callbacks Exist

The problem callbacks solve — customizing a reusable, sealed component's behavior at predefined points without modifying or subclassing it; why source modification breaks reuse and whole-type factory override is the wrong tool for a small point-tweak; and how callback hooks let multiple independent customizations coexist on a VIP without forking it.

Callbacks · Module 22 · Page 22.1

The Engineering Problem

You have a reusable component — a vendor VIP driver, a shared agent, a team-wide monitor — and you need it to do something extra at one specific point: corrupt a packet before it's driven (error injection), log a transaction after it's received, tweak a field for your protocol variant. The catch: you don't own the component's source, or modifying it breaks reuse. So how do you extend a sealed component at a specific point without changing it? The obvious answers fail. Modify the source — you'll re-patch it on every vendor update, and forks diverge. Factory-override the whole class — you'll re-implement the component's entire drive logic just to inject one line, and re-sync it on every update, and only one override can win, so multiple teams' tweaks conflict. What's missing is a way to augment the component at predefined points, additively, with multiple independent customizations coexisting, without touching the component. That mechanism is callbacks. The problem this chapter solves is why callbacks exist: the need to customize a reusable component at hooks without modifying or subclassing it, why the alternatives fall short, and how callbacks preserve reuse.

Callbacks are a hook mechanism for customizing a reusable component's behavior at predefined points without modifying or subclassing it. The component's author places callback hooks (call-out points) at strategic moments — before driving, after receiving, on a decision — and users write callback classes (extending a callback base) with methods for those hooks, register them, and the component calls them at the right moment. The component is closed for modification (sealed, reusable) but open for extension (through its hooks) — the Open/Closed Principle in hardware verification. Callbacks are horizontal, additive, and point-specific: they augment the base behavior at a point, multiple independent callbacks coexist at the same hook, and each user customizes without forking the component. This contrasts with factory override, which is vertical, substitutive, and whole-type: it replaces a class (you re-implement its methods), one override wins, suited to swapping a type, not injecting a tweak at a point. The payoff is reuse preservation: a VIP ships once with hooks, and every user customizes it non-intrusively, so the VIP stays reusable and the customizations stay separate and survive updates. This chapter is why callbacks exist: the customization problem, the failed alternatives, the hook mechanism, callbacks-versus-override, and the reuse payoff.

Why do callbacks exist — what problem do they solve (customizing a sealed reusable component at predefined points without modifying or subclassing it), why do source modification and whole-type factory override fall short, and how do callback hooks let multiple independent customizations coexist on a VIP without forking it?

Motivation — why a sealed component still needs to be customized

A reusable component is valuable precisely because it's sealed and shared — but real use demands per-user customization. The tension is the reason callbacks exist:

  • Reusable components are sealed, but uses vary. A VIP driver is written once to be correct and reusable — but each test, each team, each project needs slightly different behavior (error injection, custom checks, logging, protocol variants). The component can't anticipate every use in its base, yet must allow them.
  • Modifying the source destroys reuse. Editing a shared/vendor component for your tweak means every vendor update re-applies your patch, forks across teams diverge, and the "reusable" component becomes N divergent copies. The whole point of reuse is lost.
  • Whole-type override is the wrong granularity. Factory override (Module 6) replaces a class — great for swapping a type, but heavyweight for a one-point tweak: you re-implement the component's complex logic just to add one line, re-sync on updates, and only one override can win.
  • Customizations must coexist. Often several independent tweaks are needed at once — error injection and logging and a coverage hook, from different authors. A single override can't hold them all; you need a mechanism where multiple, independent customizations stack.
  • The component author can anticipate extension points. The author knows where customization is likely (before driving, after receiving) — so the right design is to expose hooks at those points and let users fill them, decoupling the base from the extensions.

The motivation, in one line: a reusable component is sealed (so it stays reusable) but uses vary (each needs a tweak at a point), and the alternatives failmodifying source breaks reuse, whole-type override re-implements logic just to inject a tweak and allows only one — so callbacks exist to let the author expose hooks and users inject multiple independent customizations at those points without touching the component, preserving reuse.

Mental Model

Hold callbacks as labeled ports on a sealed appliance — users plug in accessories at the ports without opening the appliance, multiple accessories share a port, and the appliance runs its base function and invokes whatever's plugged in:

A reusable component is a sealed appliance you can't (and shouldn't) open. The manufacturer, knowing people will want to extend it, builds in labeled ports at the points where extension makes sense — a port that fires just before the appliance acts, a port just after. To customize it, you don't pry it open and rewire it (modifying the source) and you don't buy a whole different appliance and rebuild it from scratch to change one thing (whole-type override). You build a small accessory and plug it into the right port. The appliance runs its normal function, and at each port it invokes whatever accessories are plugged in, then continues. Several people can plug different accessories into the same port — an error-injector and a logger and a coverage-tap all on the pre-act port — and they all run, independently, none aware of the others. And because you never opened the appliance, when the manufacturer ships a new model, your accessory still plugs into the same port: your customization survives the update, and it never forked the appliance. Picture a reusable component as a sealed appliance you can't (and shouldn't) open. The manufacturer, knowing people will want to extend it, builds in labeled ports at the points where extension makes sense — a port that fires just before the appliance acts, a port just after. To customize it, you don't pry it open and rewire it (modifying the source — breaks reuse) and you don't buy a whole different appliance and rebuild it from scratch to change one thing (whole-type override — re-implements everything for one tweak). You build a small accessory and plug it into the right port. The appliance runs its normal function, and at each port it invokes whatever accessories are plugged in, then continues. Several people can plug different accessories into the same port — an error-injector and a logger and a coverage-tap all on the pre-act port — and they all run, independently, none aware of the others (multiple customizations coexist). And because you never opened the appliance, when the manufacturer ships a new model, your accessory still plugs into the same port: your customization survives the update, and it never forked the appliance. The appliance is closed for modification (you can't open it) but open for extension (it has ports) — and the accessory adds behavior at a point, without replacing the appliance.

So callbacks are labeled ports on a sealed appliance: the component author exposes hooks (ports) at the extension points, users plug in callback objects (accessories) without opening the component, multiple customizations share a port and run independently, and the appliance runs its base function and invokes whatever's plugged in. You extend by plugging in (additive, at a point, multiple), never by prying it open (modifying — breaks reuse) or rebuilding it (whole-type override — wrong granularity) — and the customization survives updates because the appliance is untouched. Expose hooks, plug in accessories at the ports, and never open the appliance to customize it.

Visual Explanation — callbacks as hooks on a sealed component

The defining picture is the hook: the component calls out at predefined points to registered callback objects, which inject behavior — without the component being modified.

Callbacks: component places hooks, users register callback classes, component calls them at the pointsThe component (sealed, reusable)the VIP driver/monitor — written once, never modified; it places callback hooks at strategic points in its executionthe VIP driver/monitor — written once, never modified; it places callback hooks at strategic points in its executionCallback hooks (the exposed points)call-out points the author placed — e.g. pre-drive, post-receive; uvm_do_callbacks(...) invokes the registered callbackscall-out points the author placed — e.g. pre-drive, post-receive; uvm_do_callbacks(...) invokes the registered callbacksCallback classes (user-supplied behavior)users extend a callback base and implement the hook methods — error injection, logging, a coverage tapusers extend a callback base and implement the hook methods — error injection, logging, a coverage tapRegistration (connects them)users register callback objects with the component's callback pool; multiple independent callbacks coexistusers register callback objects with the component's callback pool; multiple independent callbacks coexist
Figure 1 — callbacks as hooks the component calls at predefined points. The component author places callback hooks at strategic moments in its execution — for example, just before it drives and just after it receives. Users write callback classes with methods for those hooks and register them. At each hook, the component calls every registered callback's method, which injects custom behavior, then the component continues its base behavior. The component is closed for modification but open for extension: it is never edited, yet its behavior is customized at the exposed points.

The figure shows callbacks as hooks. The component (the brand-colored top — sealed, reusable) is written once and never modified; it places callback hooks at strategic points in its execution. Callback hooks (the warning-colored layer — the exposed points) are the call-out points the author placed (e.g. pre-drive, post-receive); the component invokes the registered callbacks there (via uvm_do_callbacks(...)). Callback classes (the success-colored layer — user-supplied behavior) are written by users extending a callback base and implementing the hook methods (error injection, logging, a coverage tap). Registration connects them — users register callback objects with the component's callback pool, and multiple independent callbacks coexist. The crucial reading is the decoupling and direction of control: the component (which it doesn't own the customizations) calls out to the callbacks (which don't own the component) at the hooks — so the base behavior and the injected behavior are fully separated, connected only at the registration and the hook call. The component author anticipates extension (placing hooks) without knowing the specific customizations; the users supply customizations (callback classes) without modifying the component. This is the Open/Closed Principle: the component is closed for modification (the brand-colored sealed component is never edited) but open for extension (the warning-colored hooks expose the extension points, the success-colored callbacks fill them). The multiplicity (multiple callbacks at a hook) is key — it's what lets independent customizations coexist (the registration pool holds them all, the hook calls them all). The diagram is the callback anatomy: component (places hooks) → hooks (call-out points) → callback classes (user behavior) → registration (connects, multiple) — the mechanism that customizes a sealed component at points without modifying it. The component calls out at its hooks to registered callbacks; the base is untouched, the behavior is extended.

RTL / Simulation Perspective — the alternatives, and why callbacks win

In code, the contrast is stark: modifying source and whole-type override versus a callback. The example shows all three for the same task — injecting error into a sealed VIP driver.

three ways to inject error into a sealed VIP driver — only the callback preserves reuse
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// THE TASK: corrupt one byte before the VIP driver drives — at the driver's PRE-DRIVE point.
 
// ✗ (1) MODIFY THE SOURCE — edit the vendor's driver. Breaks reuse.
//    // inside vendor_driver.sv (you don't own this!):
//    task drive(txn t); t.data ^= 8'hFF; /* your hack */ ... endtask
//    // → every vendor update re-applies this patch; forks across teams diverge
 
// ✗ (2) WHOLE-TYPE FACTORY OVERRIDE — re-implement the entire driver to add one line.
class my_driver extends vendor_driver;
  task drive(txn t);
    t.data ^= 8'hFF;          // the one line you wanted...
    super.drive(t);           // ...but you inherit/re-call the COMPLEX base logic, and only ONE
  endtask                     //    override can win → multiple teams' tweaks CONFLICT
endclass
// uvm_factory override: vendor_driver -> my_driver  (heavyweight for a one-point tweak)
 
// ✓ (3) CALLBACK — inject ONLY the custom behavior at the driver's exposed PRE-DRIVE hook.
class err_inject_cb extends vendor_driver_callback;   // extend the VIP's callback base
  virtual function void pre_drive(vendor_driver drv, txn t);
    t.data ^= 8'hFF;          // ONLY the custom behavior — no re-implementation of drive()
  endfunction
endclass
// register it (multiple callbacks coexist):
//   uvm_callbacks#(vendor_driver, vendor_driver_callback)::add(drv, err_inject_cb::type_id::create());
// the VIP is UNTOUCHED, survives updates, and a logger callback can be added alongside

The code shows the three approaches for the same taskcorrupt a byte before the VIP driver drives. (1) Modify the sourceedit the vendor's drive() task inside vendor_driver.sv. But you don't own it: every vendor update re-applies this patch, and forks across teams divergereuse broken. (2) Whole-type factory overridesubclass vendor_driver, re-implement drive() to add t.data ^= 8'hFF then super.drive(t). The one line you wanted, but you re-call (and depend on) the complex base logic, re-sync on updates, and only one override can win — so multiple teams' tweaks conflict. (3) Callback — extend the VIP's callback base (vendor_driver_callback), implement only the pre-drive hook (t.data ^= 8'hFF), and register it. Only the custom behaviorno re-implementation of drive() — the VIP is untouched, survives updates, and a logger callback can be added alongside (multiple coexist). The shape to carry: for a point-tweak on a sealed component, modifying source (1) breaks reuse, whole-type override (2) re-implements logic for one line and allows only one, and the callback (3) injects only the custom behavior at the exposed hook, non-intrusively, multiply, surviving updates. The callback is minimal (just the hook method), additive (base drive() runs, plus your injection), multiple (a logger callback too), and reuse-preserving (the VIP .sv is never edited). The override (2) looks close — but it replaces the type (vertical), depends on the base internals, and only one can register; the callback augments at a point (horizontal) and stacks. For a point-tweak on a sealed component, the callback injects only the tweak at the hook — source-edit breaks reuse, whole-type override is the wrong granularity.

Verification Perspective — callbacks versus factory override

The sharpest distinction is callbacks versus factory overrideboth customize, but differently, and choosing right matters. Seeing the two axes clarifies when each fits.

Callbacks (augment at a point, additive, multiple, horizontal) vs factory override (replace a type, substitutive, single, vertical)augment at apoint?replace a type?additive · point · multipleadditive ·point ·…substitutive · whole-type · singlesubstitutive ·whole-type ·…Customization needwhich mechanism?Callbackaugment at a hookFactory overridereplace a classAdditive, point, multiplebase + injection; horizontal;several coexistSubstitutive, whole-type,singlenew instead of base; vertical;one wins12
Figure 2 — callbacks versus factory override: augment-at-a-point versus replace-a-type. A callback augments the base behavior at a specific hook: it is additive (base behavior plus the injection), point-specific (one moment in execution), and multiple (several independent callbacks coexist at the hook) — horizontal extension. A factory override replaces a whole class with a subclass: it is substitutive (new behavior instead of the base), whole-type (the entire component), and single (one override wins) — vertical extension. Use callbacks to inject a tweak at a point with multiple customizations; use factory override to swap a component's type wholesale.

The figure shows callbacks versus factory override. A callback augments the base behavior at a specific hook: it is additive (base behavior plus the injection), point-specific (one moment in execution), and multiple (several independent callbacks coexist at the hook) — horizontal extension. A factory override replaces a whole class with a subclass: it is substitutive (new behavior instead of the base), whole-type (the entire component), and single (one override wins) — vertical extension. The verification insight is that these are different tools for different jobs, and the customization's nature decides which. Use a callback when you want to inject a tweak at a point while keeping the base behavior, especially with multiple independent customizations (error injection and logging and a coverage tap). Use a factory override when you want to swap a component's type wholesale — a different driver implementation, a different transaction subtype — where replacing the whole class is the point. Misusing them is the common error: overriding the whole type to inject a one-point tweak (Figure's foprops for a cbprops job) re-implements the component's logic just to add a line and allows only one — the wrong granularity; trying to callback a wholesale type swap would be awkward (you'd need a hook at every point). The brand-colored mechanisms map to their success/default-colored property profilesadditive/point/multiple (callback) versus substitutive/whole-type/single (override) — and matching the customization to the right profile is the skill. The defining difference is additive-at-a-point (callback — base runs, plus your hook) versus substitutive-of-a-type (override — your class runs instead). This is why callbacks exist as a separate mechanism: factory override already existed (Module 6), but it can't do additive, multiple, point-specific customization — that gap is exactly what callbacks fill. The diagram is the tool choice: augment-at-a-point/additive/multiple → callback; replace-a-type/substitutive/single → factory overridecomplementary customization mechanisms. Callbacks augment at a point additively and multiply; factory override replaces a type wholesale and singly — match the need to the mechanism.

Runtime / Execution Flow — the component calling out to callbacks

At run time, the component runs its base behavior and, at each hook, calls every registered callback, then continues. The flow shows the call-out.

Component execution calling out to callbacks at each hook then resumingcomponent runs base behavior → reaches a hook → invokes every registered callback for that hook (in order) → callbacks inject behavior → component resumes base behaviorcomponent runs base behavior → reaches a hook → invokes every registered callback for that hook (in order) → callbacks inject behavior → component resumes base behavior1Component runs its base behaviorthe sealed component executes its normal logic up to a callbackhook (e.g. just before driving).2Reaches a hook → calls the callbacksat the hook, the component invokes every registered callback'smethod for that hook, in registration order.3Callbacks inject behavioreach callback runs its custom behavior — error injection, logging —operating on the shared transaction/context.4Component resumes base behaviorafter all callbacks for the hook have run, the component continues— augmented at the point, not replaced.
Figure 3 — the component calls out to its callbacks at each hook, then continues. The component runs its base behavior up to a callback hook. At the hook, it invokes every registered callback's method for that hook, in order — each injecting its custom behavior (error injection, logging) and operating on the shared transaction or context. After all callbacks for that hook have run, the component resumes its base behavior. The callbacks run interleaved with the base behavior at the predefined points, augmenting it without replacing it.

The flow shows the component calling out to its callbacks. Base (step 1): the sealed component executes its normal logic up to a callback hook (e.g. just before driving). Hook (step 2): at the hook, the component invokes every registered callback's method for that hook, in registration order. Inject (step 3): each callback runs its custom behavior — error injection, logging — operating on the shared transaction/context. Resume (step 4): after all callbacks for the hook have run, the component continuesaugmented at the point, not replaced. The runtime insight is the interleaving and direction of control: the base behavior runs, and at the predefined points it yields to the callbacks (which inject), then takes back control and continues. The callbacks don't replace the base — they run alongside it, at the hooks, augmenting it. This is the runtime meaning of "additive at a point" (Figure 2): the base drive() still runs, but before it (at the pre_drive hook), your error-injection callback modified the transaction. The multiplicity is visible here too: at one hook, several callbacks run in order (error-injector, then logger, then coverage-tap) — all getting their turn, independently. The contrast with factory override is runtime-clear: an override would replace drive() entirely (the base never runs as-is); a callback lets the base run and injects at the hook. So the callback mechanism is a cooperative call-out: the component (which owns the base flow) invites the callbacks (which own the injections) at the hooks, and control returns to the component. This is why callbacks are non-intrusive — the component's flow is intact; the callbacks are guests it calls at known moments. The flow is the call-out: base → hook (call all callbacks) → inject → resume base — the component augmented at its hooks, control always returning to the base. The component runs its base flow and calls out to the registered callbacks at each hook, then resumes — augmenting, not replacing.

Waveform Perspective — base behavior and injected behavior interleaved

The call-out is visible on an execution timeline: the base runs, pauses at a hook to run the callback (injecting), then resumes. The waveform shows a driver's cycle with pre- and post-drive hooks firing.

The driver runs its base flow and calls out to callbacks at the pre- and post-drive hooks

12 cycles
The driver runs its base flow and calls out to callbacks at the pre- and post-drive hooksdriver runs its base flow (base_exec) — the sealed component's normal logicdriver runs its base f…pre-drive hook fires → error-injection callback runs (cb_inject) → modifies the txnpre-drive hook fires →…driver drives the (now-modified) transaction (drive_bus=D1) — base behaviordriver drives the (now…post-drive hook fires → logging callback runs (cb_log) — multiple hooks, base intactpost-drive hook fires …clkbase_exechook_precb_injectdrive_bus------D1D1--------------hook_postcb_logt0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — base behavior and injected behavior interleaved at the hooks. The driver executes its base flow (base_exec). Just before it drives, the pre-drive hook fires (hook_pre) and the registered error-injection callback runs (cb_inject), modifying the transaction. The driver then drives (drive_bus) the modified transaction. After driving, the post-drive hook fires (hook_post) and a logging callback runs (cb_log). The base flow resumes throughout — the callbacks run at the predefined points, augmenting the base behavior, not replacing it. The component is never modified; its behavior is customized at the hooks.

The waveform shows base and injected behavior interleaved at the hooks. The driver executes its base flow (base_exec). Just before it drives, the pre-drive hook fires (hook_pre) and the registered error-injection callback runs (cb_inject), modifying the transaction. The driver then drives (drive_bus=D1) the modified transaction. After driving, the post-drive hook fires (hook_post) and a logging callback runs (cb_log). The base flow resumes throughout. The crucial reading is the interleaving: the base behavior (base_exec) is the backbone, and at the predefined points (hook_pre, hook_post) it yields to the callbacks (cb_inject, cb_log), which inject — then the base resumes. The error-injection callback runs before the drive (so the drive carries the modified data), and the logging callback runs after (so it logs what was driven) — the hooks are at the meaningful points, and the callbacks augment the base there. The picture to carry is that callbacks are interleaved with the base at the hooks, not a replacement of it: the driver still drives (drive_bus=D1), but what it drives was injected by a callback at the pre-drive hook, and the drive was logged by a callback at the post-drive hook. The component is never modified — its behavior is customized at the hooks by the registered callbacks. This is the runtime signature of the callback mechanism: the base flow with callback call-outs at the predefined points, augmenting the behavior without replacing it. Reading the timeline this way — where does the base run, and where does it call out to a callback that injects? — is seeing why callbacks customize without modifying. The hooks firing within the base flow, callbacks injecting, base resuming is the signature of non-intrusive, point-specific extension. The base flow runs and calls out to callbacks at the hooks — injected behavior interleaved with base behavior, the component untouched.

DebugLab — the forked VIP that diverged across five teams

A vendor VIP forked five ways to inject small tweaks — a maintenance nightmare callbacks would have prevented

Symptom

A vendor shipped a reusable bus-agent VIP, used by five teams. Each team needed a small customization: team A wanted error injection before driving, team B wanted transaction logging, team C a protocol-variant tweak, and so on. None of them used the VIP's callback hooks — instead, each copied the VIP source into their own tree and edited the driver to add their tweak. For a while it worked. Then the vendor shipped a major VIP update (a bug fix the whole company needed). Each of the five teams had to manually re-apply their edits onto the new VIP source — by hand, carefully, re-testing. Two teams' edits conflicted with the vendor's changes and broke. The "reusable" VIP had become five divergent forks, and every vendor update triggered a five-way manual re-patch — a recurring nightmare that grew with every release.

Root cause

Each team customized the VIP by modifying its source (forking) — instead of injecting their tweak through the VIP's callback hooks — so every tweak broke reuse, diverged across teams, and had to be manually re-applied on every update:

why five small tweaks became a five-way maintenance nightmare
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ FORK the VIP source per team (each edits the driver for one tweak):
  // team A's tree: vendor_driver.sv edited → t.data ^= mask before drive
  // team B's tree: vendor_driver.sv edited → $display logging after drive
  // ...5 divergent copies of a "reusable" VIP
  // vendor ships an update → ALL 5 teams manually re-apply their edits → conflicts, breakage, re-test
  // the customizations are TANGLED into the VIP source → can't update one without the other
 
✓ INJECT each tweak through the VIP's callback hooks (no fork, coexisting):
  class err_cb extends bus_driver_callback;   virtual function void pre_drive(...); t.data ^= mask; endfunction endclass
  class log_cb extends bus_driver_callback;   virtual function void post_drive(...); `uvm_info(...) endfunction endclass
  // register per test: uvm_callbacks#(bus_driver, bus_driver_callback)::add(drv, err_cb::create());
  // the VIP source is UNTOUCHED → a vendor update just drops in → callbacks still attach to the same hooks
  // multiple teams' callbacks COEXIST on the same VIP → no forks, no re-patching, no conflicts

This is the fork-instead-of-callback bug — the quintessential motivation for callbacks. Five teams each needed a small tweak on a reusable VIP, and each forked the VIP source and edited the drivertangling their customization into the VIP's code. The consequence unfolds on the next vendor update: because the customizations live inside the VIP source, updating the VIP means every team manually re-applies their edits onto the new source, by hand, with conflicts and breakage — a five-way re-patch that recurs on every release and grows over time. The "reusable" VIP has become five divergent forks that can't benefit from updates cleanly. The root error is customizing by modifying the sourceexactly what callbacks exist to prevent. The fix is to inject each tweak through the VIP's callback hooks: an err_cb (extending the VIP's callback base) implements pre_drive, a log_cb implements post_drive, and each is registered per test. Now the VIP source is untouched — a vendor update just drops in (the callbacks still attach to the same hooks), multiple teams' callbacks coexist on the same VIP (no forks), and there's no re-patching, no conflicts. The general lesson, and the chapter's thesis: to customize a reusable component at a point, use its callback hooks — not source modificationmodifying the source (forking) tangles the customization into the component, breaking reuse: every update re-applies the edit (by hand, with conflicts), forks diverge across teams, and the "reusable" component becomes N divergent copies; a callback injects only the custom behavior at the exposed hook, non-intrusively, so the component stays untouched (updates drop in cleanly), customizations stay separate (no tangling), and multiple coexist (no single-override conflict) — callbacks exist precisely to make point-customization possible without modifying the component, preserving reuse across updates and teams. A small tweak that forks a reusable component trades a one-line callback for a permanent maintenance tax — extend through the hooks, never by editing the source.

Diagnosis

The tell is a reusable component forked or source-edited for small customizations. Diagnose fork-instead-of-callback:

  1. Look for edited copies of a shared/vendor component. A customization made by editing the component's source is a fork that breaks reuse and re-patches on updates.
  2. Check whether the component exposes callback hooks. If the tweak is at a point the component calls out (pre/post an action), a callback is the intended mechanism.
  3. Count the divergent copies. Multiple teams each editing the source means N forks that diverge and conflict — exactly what callbacks prevent.
  4. Trace update pain. If every vendor update triggers manual re-patching, the customizations are tangled into the source and should be callbacks instead.
Prevention

Extend through hooks, never by editing the source:

  1. Inject point-customizations through callback hooks. A callback class implementing the hook method injects only the tweak, leaving the component untouched.
  2. Never fork a reusable component for a tweak. Editing the source breaks reuse and creates a permanent re-patch tax on every update.
  3. Use callbacks for additive, multiple, point-specific tweaks. Error injection, logging, and coverage taps coexist as separate callbacks on the same hooks.
  4. Reserve factory override for wholesale type swaps. A different implementation entirely is an override; a tweak at a point is a callback.

The one-sentence lesson: to customize a reusable component at a point, use its callback hooks, not source modification — forking the source tangles the customization into the component, breaking reuse so every update re-applies the edit by hand and forks diverge across teams, while a callback injects only the custom behavior at the exposed hook non-intrusively, leaving the component untouched, the customizations separate, and multiple coexisting; callbacks exist precisely to make point-customization possible without modifying the component.

Common Mistakes

  • Modifying a reusable component's source for a tweak. Forking breaks reuse and re-patches on every update; inject the tweak through a callback hook instead.
  • Using whole-type factory override for a one-point tweak. It re-implements the component's logic just to add a line and allows only one override; use a callback to augment at the point.
  • Expecting one override to hold multiple customizations. Only one factory override wins; multiple independent tweaks need callbacks, which coexist at a hook.
  • Confusing augment with replace. Callbacks add behavior at a point (base still runs); factory override replaces the type (base doesn't run as-is) — pick by which you need.
  • Not exposing hooks when authoring a reusable component. A component meant for reuse should place callback hooks at likely extension points so users can customize without forking.
  • Forgetting callbacks survive updates. Because the component is untouched, callbacks re-attach to the same hooks after a vendor update — forks don't.

Senior Design Review Notes

Interview Insights

Callbacks exist to customize a reusable, sealed component's behavior at predefined points without modifying or subclassing it. The problem is a tension inherent in reuse: a component like a vendor VIP driver is written once to be correct and reusable, so it's sealed — you don't own its source, or modifying it breaks reuse — but real uses vary, and each test or team needs a small tweak at a specific point, like corrupting a packet before driving, logging after receiving, or a protocol-variant adjustment. The obvious ways to customize all fail. Modifying the source destroys reuse: you'd re-apply your patch on every vendor update, and forks across teams diverge into N divergent copies. Whole-type factory override is the wrong granularity: to inject one line, you re-implement the component's entire complex logic in a subclass, re-sync it on every update, and only one override can win, so multiple teams' tweaks conflict. What's missing is a way to augment the component at predefined points, additively, with multiple independent customizations coexisting, without touching the component. Callbacks are that mechanism. The component's author places callback hooks — call-out points — at strategic moments, like pre-drive and post-receive. Users write callback classes extending a callback base, implement the hook methods with their custom behavior, and register them, and the component calls them at the right moment. So the component is closed for modification but open for extension — the Open/Closed Principle. The mental model is a sealed appliance with labeled ports: you plug accessories into the ports without opening the appliance, multiple accessories share a port, and the appliance runs its base function and invokes whatever's plugged in. The payoff is reuse preservation: the VIP ships once with hooks, every user customizes it non-intrusively, so it stays reusable, the customizations stay separate, and they survive updates because the component is never touched. Callbacks fill the exact gap that source modification and whole-type override can't: additive, multiple, point-specific customization of a sealed component.

A callback augments the base behavior at a specific hook — additive, point-specific, and multiple — while a factory override replaces a whole class with a subclass — substitutive, whole-type, and single. They're different tools for different jobs. A callback is horizontal extension: the component runs its base behavior, and at a predefined hook it calls your callback, which injects custom behavior, then the base resumes. The base still runs — your callback adds to it at a point. Several independent callbacks can register on the same hook and all run, in order, so error injection and logging and a coverage tap coexist. You reach for a callback when you want to inject a tweak at a specific moment while keeping the base behavior, especially with multiple customizations. A factory override is vertical extension: you write a subclass that replaces the original class entirely, and the factory substitutes your type wherever the original would be created. Your class runs instead of the base — you've swapped the type. Only one override of a given type wins. You reach for a factory override when you want to swap a component's implementation or a transaction's subtype wholesale — a genuinely different thing in place of the original. The key distinction is additive-at-a-point versus substitutive-of-a-type. The common mistake is using a factory override for a one-point tweak: you subclass the whole driver and re-implement its drive method just to add one line before calling super, which re-implements complex logic for a tiny change, depends on the base internals, and allows only one override, so multiple teams conflict. That's a callback's job — inject the one line at the pre-drive hook, leave the rest untouched, and let multiple callbacks coexist. Conversely, using callbacks to achieve a wholesale type swap would be awkward, needing a hook at every point. This is actually why callbacks exist as a separate mechanism: factory override already existed, but it can't do additive, multiple, point-specific customization, and that gap is exactly what callbacks fill. So you match the customization to the mechanism: augment at a point with multiple tweaks, use a callback; replace a type wholesale, use a factory override.

Callbacks preserve reuse because the component is never modified — the customization lives in a separate callback class registered externally — whereas source modification tangles the customization into the component, breaking reuse. When you customize by editing a reusable component's source, you've created a fork: your tweak is now inside the component's code, in your own copy. This breaks reuse in several compounding ways. First, updates: when the vendor or the owning team ships a new version, you have to manually re-apply your edits onto the new source, by hand, carefully, with conflicts and re-testing — because your changes are entangled with theirs. Second, divergence: if five teams each edit the source for their own tweak, you have five divergent copies of a supposedly reusable component, each maintained separately, none able to take updates cleanly. Third, the maintenance tax recurs and grows with every release. The component stops being reusable and becomes N forks. Callbacks avoid all of this by keeping the customization completely separate. You write a callback class — extending the component's callback base and implementing a hook method with just your custom behavior — and you register it externally, at runtime, without touching the component's source. The component stays exactly as shipped. So when an update arrives, it drops in cleanly — there's nothing to re-apply, because your customization was never in the source; the callback simply re-attaches to the same hooks in the new version. Multiple teams' callbacks coexist on the same untouched component, registered independently, with no forks and no conflicts. The customization and the component evolve separately. This is the essence of why callbacks exist: they let you extend a component without modifying it, which is the only way to customize while keeping reuse intact. The component is closed for modification — you never edit it — but open for extension — you attach callbacks at its hooks. Source modification trades a small immediate convenience for a permanent reuse-destroying maintenance burden; callbacks pay neither cost.

The three actors are the component that calls the hooks, the callback class that defines the custom behavior, and the registration that connects them — typically through a callback pool. The first actor is the component — the reusable, sealed thing being customized, like a VIP driver or monitor. Its role is to place callback hooks at strategic points in its execution and to call out at those hooks during operation. The component author decides where extension makes sense — pre-drive, post-receive, on a decision — and inserts a hook there that invokes the registered callbacks, conventionally via a macro like uvm_do_callbacks. The component doesn't know what the callbacks will do; it just provides the call-out points. The second actor is the callback class — the user-supplied behavior. The user extends the component's callback base class and implements the hook methods with their custom behavior, like corrupting a transaction in pre_drive or logging in post_drive. This class is separate from the component, owned by the user, and contains only the injected behavior, not any re-implementation of the component. The third actor is the registration, the connection between them, typically a callback pool keyed by the component. The user creates a callback object and registers it with the component through the pool, often via uvm_callbacks add. The pool holds all the callbacks registered for a component, so when the component reaches a hook and calls out, the mechanism looks up the registered callbacks in the pool and invokes each one's relevant method, in registration order. The reason there are three actors rather than two is decoupling: the component and the callbacks don't know about each other directly — the registration pool sits between them, connecting them at runtime. The component calls out generically, the pool routes to the registered callbacks, and the callbacks inject behavior. This decoupling is what lets the component stay sealed and unaware of specific customizations while users attach behavior externally, and it's what lets multiple independent callbacks coexist, since the pool can hold many for one component. The detailed mechanics of declaring hooks, the callback base classes, pool registration, and ordering are the substance of how callbacks work, but the three-actor structure — component calls, callback defines, registration connects — is the shape.

You author a component with callback hooks when it's meant to be reused across tests, teams, or projects, and you can anticipate points where users will want to customize its behavior without modifying it. The whole value of callbacks is realized at authoring time: if you're writing a reusable component — a driver, monitor, agent, or any VIP others will use — you should ask where users are likely to want to inject behavior, and place callback hooks there. Common points are just before an action the user might want to alter, like pre-drive where they could inject errors or modify a transaction, and just after an event they might want to observe, like post-receive where they could log or sample coverage. By placing hooks at these strategic points, you make the component closed for modification but open for extension — users can customize at the hooks without ever editing or forking your source. This is essential for reusable components because you can't anticipate every specific use in the base, but you can anticipate where extension will be wanted, and exposing hooks there lets users supply the specifics. The benefit is that your component stays a single, maintainable, reusable artifact: users attach callbacks externally, updates ship cleanly, and multiple users' customizations coexist, none forking your code. If you don't expose hooks, users who need a tweak are forced to fork the source or do a heavy whole-type override, which breaks reuse and creates the divergence and re-patching problems. So as a reusable-component author, providing callback hooks is part of good design — it's how you let your component be customized while keeping it reusable. You wouldn't necessarily add hooks to a one-off, project-specific component that no one else will reuse, since there's no reuse to preserve. But for anything intended as shared infrastructure or VIP, anticipating extension points and exposing callback hooks is the design that makes it genuinely reusable and customizable at once. The senior-review version of this is checking that an authored reusable component exposes hooks where users will predictably want to extend, so they never have to modify it.

Exercises

  1. Pick the mechanism. For injecting error before a VIP drives, and for swapping in a different driver implementation, name callback or factory override for each and why.
  2. Spot the fork. A team edited a vendor monitor's source to add logging. Explain the reuse problem and the callback alternative.
  3. Coexist the tweaks. Error injection, logging, and a coverage tap are all needed on a driver. Explain why callbacks handle this and a single override doesn't.
  4. Author the hooks. For a reusable driver, name two points where you'd place callback hooks and what users might inject there.

Summary

  • Callbacks exist to customize a reusable, sealed component's behavior at predefined points without modifying or subclassing it — the author places hooks, users write and register callback classes that inject behavior at those hooks, and multiple independent customizations coexist.
  • They are horizontal, additive, point-specific extension — the component is closed for modification (reusable, untouched) but open for extension (through its hooks), the Open/Closed Principle.
  • The failed alternatives: modifying the source breaks reuse (re-patched every update, forks diverge); whole-type factory override re-implements the component's logic just to inject a tweak, must be re-synced, and allows only one — so multiple tweaks conflict.
  • Callbacks contrast with factory override: callbacks augment at a point (additive, multiple, horizontal — base behavior plus injected); override replaces a type (substitutive, single, vertical — new instead of base) — match the customization to the mechanism.
  • The durable rule of thumb: to customize a reusable component at a point, extend through its callback hooks, never by editing the source or whole-type-overriding for a tweak — a callback injects only the custom behavior at the exposed point non-intrusively, so the component stays untouched (updates drop in cleanly), the customizations stay separate, and multiple coexist; callbacks exist precisely to make additive, multiple, point-specific customization possible without modifying the component, preserving reuse across updates and teams.

Next — UVM Callbacks: with the why established, the next chapter details the mechanism — the three actors in depth: declaring callback hooks in a component with the callback macros, writing callback classes against a callback base, registering callbacks in the typed callback pool, the uvm_do_callbacks call-out and ordering, and the canonical error-injection example end to end.