Causal event tracking SDK for Rust, based on the work of Leslie Lamport (1978).
In microservice architectures, events are generated asynchronously across multiple services. Without a causal ordering mechanism, it's impossible to answer questions like:
- Did service B process the response before service A finished writing?
- Did these two events happen in parallel, or did one cause the other?
- Why do logs show events out of order even with system timestamps?
Physical wall-clock timestamps don't solve this — each machine has its own clock with drift and no guaranteed synchronization. The result: race condition bugs that are extremely hard to reproduce and debug.
like-a-clockwork implements two complementary mechanisms from Lamport's seminal paper:
Lamport Clock — A monotonic logical counter per process. Guarantees that if event A
caused event B, then clock(A) < clock(B). Simple, lightweight, provides total ordering.
Vector Clock — A vector of counters, one per process. Detects all three causal relationships: happens-before, happens-after, and concurrent. Enables conflict detection and race condition identification.
Add to your Cargo.toml:
[dependencies]
like-a-clockwork = "0.1"Or via CLI:
cargo add like-a-clockworkuse like_a_clockwork::LamportClock;
// Each service creates its own clock
let mut order_svc = LamportClock::new("order-service");
let mut payment_svc = LamportClock::new("payment-service");
// Internal event
order_svc.tick(); // time = 1
// Sending a message: get a timestamp to propagate
let ts = order_svc.send(); // time = 2, returns LamportTimestamp
// Receiving: the other service syncs its clock
payment_svc.receive(&ts); // time = max(0, 2) + 1 = 3
// Guaranteed: payment's receive happened *after* order's send
assert!(payment_svc.time() > ts.time());use like_a_clockwork::{VectorClock, CausalityRelation};
let mut svc_a = VectorClock::new("svc-a", &["svc-b"]);
let mut svc_b = VectorClock::new("svc-b", &["svc-a"]);
// Both services process events independently
svc_a.tick(); // svc-a: {svc-a: 1, svc-b: 0}
svc_b.tick(); // svc-b: {svc-a: 0, svc-b: 1}
// Detect the relationship
match svc_a.relation(&svc_b) {
CausalityRelation::Concurrent => {
// These events are concurrent — possible race condition!
println!("conflict detected: both services wrote independently");
}
CausalityRelation::HappensBefore => println!("A caused B"),
CausalityRelation::HappensAfter => println!("B caused A"),
CausalityRelation::Equal => println!("same causal state"),
}use like_a_clockwork::{VectorClock, TracedEvent};
let mut clock = VectorClock::new("order-service", &["payment-service"]);
clock.tick();
// Wrap any domain event with causal metadata
let event = TracedEvent::new(
"order.created",
b"{\"order_id\": 42}",
clock.snapshot(),
).unwrap();
// Serialize to headers for transport (HTTP, Kafka, etc.)
let headers = event.to_headers();
// {
// "X-Causality-Vector": "order-service=1,payment-service=0",
// "X-Causality-EventId": "019476a0-b1c2-...",
// "X-Causality-EventType": "order.created",
// }
// Reconstruct on the consumer side
let received = TracedEvent::from_headers(&headers, b"{\"order_id\": 42}").unwrap();
assert_eq!(received.event_type(), "order.created");The transport layer works with plain HashMaps — no framework dependencies.
You bridge it to your HTTP/Kafka/gRPC library of choice.
use like_a_clockwork::transport::text;
use like_a_clockwork::{LamportClock, VectorClock};
use std::collections::HashMap;
// === Text transport (HTTP headers, gRPC ASCII metadata) ===
let mut clock = VectorClock::new("api-gateway", &["auth-service"]);
let ts = clock.send();
let mut headers = HashMap::new();
text::inject_vector(&mut headers, &ts).unwrap();
// headers: {"X-Causality-Vector": "api-gateway=1,auth-service=0"}
// On the receiving end
let extracted = text::extract_vector(&headers).unwrap();
assert!(extracted.is_some());use like_a_clockwork::transport::binary;
use like_a_clockwork::VectorClock;
use std::collections::HashMap;
// === Binary transport (Kafka record headers) ===
let mut clock = VectorClock::new("producer", &["consumer"]);
let ts = clock.send();
let mut headers: HashMap<String, Vec<u8>> = HashMap::new();
binary::inject_vector(&mut headers, &ts).unwrap();
// Serialized as compact msgpack bytes
let extracted = binary::extract_vector(&headers).unwrap();
assert!(extracted.is_some());use like_a_clockwork::transport::json;
use like_a_clockwork::VectorClock;
// === JSON transport (embedded _causality field) ===
let mut clock = VectorClock::new("order-service", &["inventory-service"]);
clock.tick();
let payload = serde_json::json!({"order_id": 42, "status": "created"});
let enriched = json::inject(
&payload,
&clock.snapshot(),
"order.created",
"event-123",
).unwrap();
// Result:
// {
// "order_id": 42,
// "status": "created",
// "_causality": {
// "vector": {"order-service": 1, "inventory-service": 0},
// "event_type": "order.created",
// "event_id": "event-123"
// }
// }
assert!(json::has_causality(&enriched));Each service serializes its Vector Clock in message headers. A log aggregator (Grafana Loki, Datadog, etc.) can reconstruct the causal graph and show exactly which service caused what — without relying on physical timestamps.
Two services read the same record and attempt to write. The Vector Clock detects
that the events are Concurrent before persisting — the application can apply a
merge strategy or reject with an explicit conflict.
order-service [1, 0] reads product #42
inventory-service [0, 1] reads product #42 at the same time
→ CausalityRelation::Concurrent ← both try to write
A Kafka consumer tracks the last processed Vector Clock per key. If an incoming
event is HappensBefore the already-processed one, it's discarded as a duplicate.
If it's Concurrent, it enters a conflict resolution queue.
Development middleware captures all events from a request, reconstructs the causal graph, and displays which services raced against each other in the terminal.
src/
├── lib.rs # Public re-exports
├── lamport.rs # LamportClock + LamportTimestamp
├── causality.rs # CausalityRelation enum + compare()
├── vector.rs # VectorClock + VectorTimestamp
├── event.rs # TracedEvent causal envelope
└── transport/
├── mod.rs # HeaderMap / BinaryHeaderMap traits + TransportError
├── text.rs # Key-value text serialization (HTTP, gRPC ASCII)
├── binary.rs # Key-value binary serialization (Kafka, gRPC binary)
└── json.rs # Embedded _causality in JSON payloads
- Zero framework dependencies — the transport layer works with
HashMap<String, String>andHashMap<String, Vec<u8>>. Integration with reqwest, axum, tonic, rdkafka, etc. is left to the user or future integration crates. - Correct by construction — the API enforces Lamport's clock conditions at the type level. Clocks never regress, timestamps are immutable, and ordering is deterministic.
- Serialization-ready — all types derive
Serialize/Deserializevia serde.
The SDK guarantees the three properties from Lamport's clock system:
Clock Condition: If event a caused event b, then C(a) < C(b).
Strong Clock Condition (Vector Clock): C(a) < C(b) if and only if a → b.
This enables concurrency detection — if neither C(a) < C(b) nor C(b) < C(a),
then a ∥ b.
Monotonicity: Clocks never regress. Any sequence of tick() / send() / receive()
produces strictly increasing values per node.
- Not a consensus system — does not implement Paxos or Raft.
- Not a distributed lock — does not guarantee mutual exclusion.
- Not a replacement for distributed tracing — complementary to OpenTelemetry. A Trace ID says "this request passed through these services". A Vector Clock says "this event caused that one".
- Not a message delivery guarantee — that's the transport's job (Kafka, HTTP, etc.).
- Lamport, L. (1978). Time, Clocks, and the Ordering of Events in a Distributed System. Communications of the ACM, 21(7), 558–565.
- Fidge, C. (1988). Timestamps in Message-Passing Systems That Preserve the Partial Ordering. Proceedings of the 11th Australian Computer Science Conference.
- Mattern, F. (1989). Virtual Time and Global States of Distributed Systems. Parallel and Distributed Algorithms, 215–226.
GNU General Public License v3.0 — see LICENSE for details.
like-a-clockwork — built on the work of Leslie Lamport, 1978.