diff --git a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Hlc/HybridLogicalClock.cs b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Hlc/HybridLogicalClock.cs
new file mode 100644
index 0000000..3d80201
--- /dev/null
+++ b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Hlc/HybridLogicalClock.cs
@@ -0,0 +1,33 @@
+namespace ZB.MOM.WW.LocalDb.Hlc;
+
+/// 64-bit HLC: (UTC unix-ms << 16) | 16-bit logical counter. Thread-safe.
+public sealed class HybridLogicalClock
+{
+ private readonly Func _utcNowMs;
+ private readonly Lock _lock = new();
+ private long _last;
+
+ public HybridLogicalClock(long initialValue = 0, Func? utcNowMs = null)
+ {
+ _utcNowMs = utcNowMs ?? (() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ _last = initialValue;
+ }
+
+ public long Next()
+ {
+ lock (_lock)
+ {
+ var candidate = _utcNowMs() << 16;
+ _last = candidate > _last ? candidate : _last + 1; // +1 rolls counter; overflow naturally carries into physical bits
+ return _last;
+ }
+ }
+
+ /// Merge a peer's HLC so our next stamp orders after everything we've seen.
+ public void Observe(long remote) { lock (_lock) { if (remote > _last) _last = remote; } }
+
+ public long Current { get { lock (_lock) return _last; } }
+
+ public static long PhysicalMs(long hlc) => hlc >> 16;
+ public static int Counter(long hlc) => (int)(hlc & 0xFFFF);
+}
diff --git a/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs
new file mode 100644
index 0000000..d16a6e3
--- /dev/null
+++ b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs
@@ -0,0 +1,90 @@
+using ZB.MOM.WW.LocalDb.Hlc;
+
+namespace ZB.MOM.WW.LocalDb.Tests;
+
+public sealed class HybridLogicalClockTests
+{
+ [Fact]
+ public void Next_IsStrictlyMonotonic()
+ {
+ var clock = new HybridLogicalClock();
+ var prev = clock.Next();
+ for (var i = 0; i < 1000; i++)
+ {
+ var next = clock.Next();
+ Assert.True(next > prev, $"call {i}: {next} !> {prev}");
+ prev = next;
+ }
+ }
+
+ [Fact]
+ public void Next_UsesUtcPhysicalMs()
+ {
+ const long fixedMs = 1_700_000_000_123;
+ var clock = new HybridLogicalClock(utcNowMs: () => fixedMs);
+
+ Assert.Equal(fixedMs, HybridLogicalClock.PhysicalMs(clock.Next()));
+ }
+
+ [Fact]
+ public void Next_SameMs_IncrementsCounter()
+ {
+ const long fixedMs = 1_700_000_000_000;
+ var clock = new HybridLogicalClock(utcNowMs: () => fixedMs);
+
+ var first = clock.Next();
+ var second = clock.Next();
+ var third = clock.Next();
+
+ Assert.Equal(0, HybridLogicalClock.Counter(first));
+ Assert.Equal(1, HybridLogicalClock.Counter(second));
+ Assert.Equal(2, HybridLogicalClock.Counter(third));
+ }
+
+ [Fact]
+ public void Observe_RemoteAhead_AdvancesPastRemote()
+ {
+ // Frozen wall clock well behind the remote stamp.
+ var clock = new HybridLogicalClock(utcNowMs: () => 1_000);
+ var remote = 5_000_000_000L << 16;
+
+ clock.Observe(remote);
+
+ Assert.True(clock.Next() > remote);
+ }
+
+ [Fact]
+ public void Observe_RemoteBehind_NoRegression()
+ {
+ var clock = new HybridLogicalClock(utcNowMs: () => 1_700_000_000_000);
+ var current = clock.Next();
+
+ clock.Observe(current - 1_000_000); // a stamp from the past
+
+ Assert.Equal(current, clock.Current);
+ }
+
+ [Fact]
+ public void Resume_FromPersistedValue_NeverRegresses()
+ {
+ // Persisted value is far ahead of what the (injected) wall clock yields.
+ var persisted = (2_000_000_000_000L << 16) | 0x00FF;
+ var clock = new HybridLogicalClock(initialValue: persisted, utcNowMs: () => 1_000);
+
+ Assert.True(clock.Next() > persisted);
+ }
+
+ [Fact]
+ public void CounterOverflow_RollsPhysicalForward()
+ {
+ const long ms = 1_700_000_000_000;
+ // Counter already maxed out under a frozen clock at the same physical ms.
+ var maxed = (ms << 16) | 0xFFFF;
+ var clock = new HybridLogicalClock(initialValue: maxed, utcNowMs: () => ms);
+
+ var next = clock.Next();
+
+ Assert.Equal(ms + 1, HybridLogicalClock.PhysicalMs(next));
+ Assert.Equal(0, HybridLogicalClock.Counter(next));
+ }
+}