diff --git a/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java b/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java
new file mode 100644
index 00000000000..ac9abf37522
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java
@@ -0,0 +1,152 @@
+package datadog.trace.util;
+
+import java.lang.reflect.Array;
+
+/**
+ * Open-addressed, single-array find-or-create over self-contained entries — each slot is one
+ * reference to an entry that carries its own key (and, typically, a cached hash). One array, one
+ * reference per slot: entry publication is a single reference store, so a reader sees {@code null} or
+ * a complete entry (never a torn one), and {@code final} identity fields on the entry are visible
+ * under racy publication. That sidesteps the memory-ordering / visibility problems parallel
+ * key/hash/value arrays would create — no {@code volatile}, no atomics — as long as the payload is
+ * one where a stale/lost read is benign (miss → recreate; clobber → one wins).
+ *
+ *
Static polymorphism (C++-template-style). The per-use policy is a {@link Helper} — a
+ * stateless subclass held by each caller as a {@code static final} field declared with the
+ * concrete helper type (not the {@code Helper} base):
+ *
+ *
{@code
+ * private static final MyHelper HELPER = new MyHelper(); // concrete type => exact type pinned
+ * ...
+ * V v = FlatHashtable.getOrCreate(table, key, HELPER);
+ * }
+ *
+ * Because {@code HELPER} is a compile-time-constant of an exact type at the call site, once these
+ * small {@code Support} methods inline the JIT devirtualizes and inlines {@code hash}/{@code
+ * matches}/{@code create} — each call site specializes to straight-line code, one instantiation per
+ * helper, with no CHA/type-profiling dependence. Keep the methods small so they inline; verify with
+ * {@code -XX:+PrintInlining} (the failure mode is silent: it compiles and runs, just stays
+ * megamorphic and slow). {@code Helper} is an abstract class, so a distinct final subclass is required
+ * anyway — an exact type gives the inliner an unambiguous receiver.
+ *
+ * Contract: {@code table.length} must be a power of two ({@link #capacityFor}). {@code
+ * helper.hash} should be well-distributed (this class masks it directly). Cardinality cap / overflow
+ * / a live-size counter are caller policy (this class is pure mechanism): a capped caller does
+ * {@link #get} first, and only on a miss checks its budget before {@link #getOrCreate} (so hits stay
+ * a single probe and the create path is warmup-rare).
+ */
+public final class FlatHashtable {
+ private FlatHashtable() {}
+
+ /**
+ * Per-use policy. Extend as a stateless final class and hold a {@code static final} singleton
+ * of the concrete type (see class doc) so the JIT can specialize each call site.
+ *
+ *
An abstract class (not an interface) on purpose: it forces a named helper type (no
+ * lambdas, which can blur the receiver the inliner needs), and if specialization ever misses, the
+ * fallback dispatches via {@code invokevirtual} rather than the costlier megamorphic {@code
+ * invokeinterface}. On the specialized (inlined) path the choice is a wash — this just hedges the
+ * fallback and lets shared bits be {@code final}-sealed later.
+ *
+ * @param lookup key
+ * @param stored entry — self-contained (carries its own key, ideally a cached hash)
+ */
+ public abstract static class Helper {
+ /** Hash of {@code key}; should be well-distributed (this table masks it directly). */
+ public abstract int hash(K key);
+
+ /** Whether the stored {@code value} entry is the one for {@code key}. */
+ public abstract boolean matches(K key, V value);
+
+ /** Mint a new entry for {@code key} (called once, on insert). */
+ public abstract V create(K key);
+ }
+
+ /**
+ * {@link Helper} specialized for {@code String} keys: seals a spread {@link #hash} so String-key
+ * callers write only {@link #matches} and {@link #create}. Extend as a stateless final class held in
+ * a concrete-typed {@code static final} singleton, exactly like {@link Helper} — the {@code final}
+ * hash resolves directly and the concrete subclass still specializes the same at each call site, so
+ * there's no cost to the extra layer.
+ *
+ * @param stored entry — self-contained (carries its own key, ideally a cached hash)
+ */
+ public abstract static class StringHelper extends Helper {
+ @Override
+ public final int hash(String key) {
+ final int h = key.hashCode();
+ return h ^ (h >>> 16); // spread; FlatHashtable masks this directly
+ }
+ }
+
+ /** Power-of-two capacity for a cardinality budget: {@code >= 2 * limit} (load factor <= 0.5). */
+ public static int capacityFor(int cardinalityLimit) {
+ if (cardinalityLimit <= 0) {
+ throw new IllegalArgumentException("cardinalityLimit must be positive: " + cardinalityLimit);
+ }
+ return Integer.highestOneBit(cardinalityLimit * 2 - 1) << 1;
+ }
+
+ /**
+ * Allocates a correctly-typed table for a cardinality budget ({@link #capacityFor} slots). Passing
+ * {@code type} makes the array's runtime component type {@code T} rather than {@code Object[]} —
+ * typed reads, real array-store checks, and a monomorphic element type for the JIT. Callers can't
+ * {@code new T[]} themselves under erasure; this does the one reflective allocation at construction
+ * (off any hot path). Note: this {@code create} mints the backing array; {@link Helper#create}
+ * mints an entry — different types, no ambiguity at the call site.
+ */
+ @SuppressWarnings("unchecked")
+ public static T[] create(Class type, int cardinalityLimit) {
+ return (T[]) Array.newInstance(type, capacityFor(cardinalityLimit));
+ }
+
+ /**
+ * Existing entry for {@code key}, or {@code null}. Read-only — never creates. Single probe on a
+ * hit; walks to the first empty slot (or all the way around) on a miss.
+ */
+ public static V get(V[] table, K key, Helper helper) {
+ final int mask = table.length - 1;
+ final int start = helper.hash(key) & mask;
+ int i = start;
+ for (; ; ) {
+ final V e = table[i];
+ if (e == null) {
+ return null; // empty slot terminates the probe (no tombstones)
+ }
+ if (helper.matches(key, e)) {
+ return e;
+ }
+ i = (i + 1) & mask;
+ if (i == start) {
+ return null; // wrapped ⇒ full, absent
+ }
+ }
+ }
+
+ /**
+ * Existing entry for {@code key}, or a freshly {@link Helper#create created} + inserted one.
+ * Returns {@code null} only if the table is full (no empty slot) — the caller supplies its overflow
+ * default. The insert is a single plain reference store: a concurrent clobber / double-create is
+ * acceptable only when the payload makes it benign (see class doc).
+ */
+ public static V getOrCreate(V[] table, K key, Helper helper) {
+ final int mask = table.length - 1;
+ final int start = helper.hash(key) & mask;
+ int i = start;
+ for (; ; ) {
+ final V e = table[i];
+ if (e == null) {
+ final V created = helper.create(key);
+ table[i] = created; // single-reference publish; benign clobber (see class doc)
+ return created;
+ }
+ if (helper.matches(key, e)) {
+ return e;
+ }
+ i = (i + 1) & mask;
+ if (i == start) {
+ return null; // wrapped ⇒ full
+ }
+ }
+ }
+}
diff --git a/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java b/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java
new file mode 100644
index 00000000000..9bebfa9c45a
--- /dev/null
+++ b/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java
@@ -0,0 +1,178 @@
+package datadog.trace.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class FlatHashtableTest {
+
+ /** Self-contained entry: carries its own key (the identity FlatHashtable relies on). */
+ static final class Entry {
+ final String key;
+
+ Entry(String key) {
+ this.key = key;
+ }
+ }
+
+ /** Stateless concrete helper, held as a concrete-typed static final singleton (the JIT idiom). */
+ static final class EntryHelper extends FlatHashtable.StringHelper {
+ @Override
+ public boolean matches(String key, Entry value) {
+ return key.equals(value.key);
+ }
+
+ @Override
+ public Entry create(String key) {
+ return new Entry(key);
+ }
+ }
+
+ private static final EntryHelper HELPER = new EntryHelper();
+
+ /** All keys hash to slot 0, so inserts chain by linear probing — exercises the probe path. */
+ static final class CollidingHelper extends FlatHashtable.Helper {
+ @Override
+ public int hash(String key) {
+ return 0;
+ }
+
+ @Override
+ public boolean matches(String key, Entry value) {
+ return key.equals(value.key);
+ }
+
+ @Override
+ public Entry create(String key) {
+ return new Entry(key);
+ }
+ }
+
+ private static final CollidingHelper COLLIDING = new CollidingHelper();
+
+ /** All keys hash to the last slot ({@code -1 & mask}), so probing wraps around to index 0. */
+ static final class LastSlotHelper extends FlatHashtable.Helper {
+ @Override
+ public int hash(String key) {
+ return -1;
+ }
+
+ @Override
+ public boolean matches(String key, Entry value) {
+ return key.equals(value.key);
+ }
+
+ @Override
+ public Entry create(String key) {
+ return new Entry(key);
+ }
+ }
+
+ private static final LastSlotHelper LAST_SLOT = new LastSlotHelper();
+
+ @Test
+ void capacityFor_roundsToPowerOfTwoAtLeastTwiceLimit() {
+ assertEquals(2, FlatHashtable.capacityFor(1));
+ assertEquals(8, FlatHashtable.capacityFor(4));
+ assertEquals(16, FlatHashtable.capacityFor(6)); // 6*2-1=11 -> 8 -> 16
+ }
+
+ @Test
+ void capacityFor_rejectsNonPositive() {
+ assertThrows(IllegalArgumentException.class, () -> FlatHashtable.capacityFor(0));
+ assertThrows(IllegalArgumentException.class, () -> FlatHashtable.capacityFor(-1));
+ }
+
+ @Test
+ void create_allocatesTypedTableOfCapacity() {
+ Entry[] table = FlatHashtable.create(Entry.class, 4);
+ assertEquals(8, table.length);
+ assertEquals(Entry.class, table.getClass().getComponentType());
+ }
+
+ @Test
+ void getOrCreate_insertsOnceAndReturnsTheExistingEntry() {
+ Entry[] table = FlatHashtable.create(Entry.class, 8);
+ Entry first = FlatHashtable.getOrCreate(table, "a", HELPER);
+ assertEquals("a", first.key);
+ // A second call must return the SAME instance, not mint a new one.
+ assertSame(first, FlatHashtable.getOrCreate(table, "a", HELPER));
+ assertSame(first, FlatHashtable.get(table, "a", HELPER));
+ }
+
+ @Test
+ void get_returnsNullForAbsentKey() {
+ Entry[] table = FlatHashtable.create(Entry.class, 8);
+ assertNull(FlatHashtable.get(table, "missing", HELPER));
+ FlatHashtable.getOrCreate(table, "present", HELPER);
+ assertNull(FlatHashtable.get(table, "still-missing", HELPER));
+ }
+
+ @Test
+ void getOrCreate_returnsNullWhenTableIsFull() {
+ // capacityFor(1) == 2 slots.
+ Entry[] table = FlatHashtable.create(Entry.class, 1);
+ assertTrue(FlatHashtable.getOrCreate(table, "k0", HELPER) != null);
+ assertTrue(FlatHashtable.getOrCreate(table, "k1", HELPER) != null);
+ // Both slots occupied by distinct keys -> a third distinct key finds no room.
+ assertNull(FlatHashtable.getOrCreate(table, "k2", HELPER));
+ // ...but an existing key still resolves even when full.
+ assertSame(
+ FlatHashtable.get(table, "k0", HELPER), FlatHashtable.getOrCreate(table, "k0", HELPER));
+ }
+
+ @Test
+ void stringHelper_hashIsStableForEqualKeys() {
+ assertEquals(HELPER.hash("route"), HELPER.hash(new String("route")));
+ }
+
+ @Test
+ void collision_probesPastOccupiedSlots_andResolvesEach() {
+ Entry[] table = FlatHashtable.create(Entry.class, 4); // 8 slots; COLLIDING sends all to slot 0
+ Entry a = FlatHashtable.getOrCreate(table, "a", COLLIDING);
+ Entry b = FlatHashtable.getOrCreate(table, "b", COLLIDING); // slot 0 taken -> probes to slot 1
+ Entry c = FlatHashtable.getOrCreate(table, "c", COLLIDING); // -> slot 2
+
+ assertNotSame(a, b);
+ assertNotSame(b, c);
+
+ // each resolves via probe-past-occupied + match-after-probe
+ assertSame(a, FlatHashtable.get(table, "a", COLLIDING));
+ assertSame(b, FlatHashtable.get(table, "b", COLLIDING));
+ assertSame(c, FlatHashtable.get(table, "c", COLLIDING));
+
+ // existing colliding key: found after probing, no new entry minted
+ assertSame(b, FlatHashtable.getOrCreate(table, "b", COLLIDING));
+
+ // absent key: probe past the 3 occupied slots, hit an empty slot -> null
+ assertNull(FlatHashtable.get(table, "absent", COLLIDING));
+ }
+
+ @Test
+ void collision_probeWrapsAroundToFront() {
+ Entry[] table =
+ FlatHashtable.create(Entry.class, 1); // 2 slots (0,1), mask=1; LAST_SLOT starts at 1
+ Entry k0 = FlatHashtable.getOrCreate(table, "k0", LAST_SLOT); // -> slot 1
+ Entry k1 = FlatHashtable.getOrCreate(table, "k1", LAST_SLOT); // slot 1 taken -> wraps to slot 0
+
+ assertNotSame(k0, k1);
+ assertSame(k0, FlatHashtable.get(table, "k0", LAST_SLOT));
+ // start slot 1 is occupied (no match) -> probe wraps to slot 0 -> match
+ assertSame(k1, FlatHashtable.get(table, "k1", LAST_SLOT));
+ }
+
+ @Test
+ void get_returnsNullWhenTableFullAndKeyAbsent() {
+ Entry[] table = FlatHashtable.create(Entry.class, 1); // 2 slots
+ FlatHashtable.getOrCreate(table, "k0", COLLIDING);
+ FlatHashtable.getOrCreate(table, "k1", COLLIDING); // fills slots 0 and 1
+
+ // get() probes both occupied slots, wraps back to start -> null (get's full-wrap branch)
+ assertNull(FlatHashtable.get(table, "absent", COLLIDING));
+ }
+}