Tickloom is a lightweight Java framework for building deterministic, testable distributed systems. It implements patterns from Patterns of Distributed Systems and serves as a reference implementation for the Distributed Systems Patterns Workshop.
It gives you:
Distributed systems share common needs:
Tickloom provides all of these in a single-threaded deterministic model — making tests reproducible and easier to debug.
In Tickloom, tick() represents a single lock step of execution.
Each tick processes pending work in a fixed, deterministic order, ensuring reproducibility and predictable behavior across runs.
All components run in a single main thread — there are no worker threads.
This eliminates race conditions and makes behavior easier to reason about.
The core components implement Tickable and are invoked in sequence:
SimulatedNetwork decides delivery time based on configured delays, partitions, and packet loss. Messages are delivered only when their scheduled delivery tick is reached.NioNetwork processes available SelectionKeys from Java NIO’s selector in each tick.While network and storage operations are asynchronous in nature, Tickloom models them explicitly within the tick loop:
A driver calls tick() on all components in the defined order: Network → MessageBus → Process → Storage
This enables both realistic production behavior and reproducible simulation, which is easier to test.
Tickloom models time in terms of ticks, not real-world milliseconds.
Every call to tick() advances a logical tick counter by one.
This makes timing deterministic and reproducible across test runs and simulations.
Timeouts are configured in terms of the number of ticks before they expire — similar to the approach used in etcd and TigerBeetle:
5 ticks, and the current tick counter is 100, the timeout will trigger at tick 105.tick() in a loop, without waiting in real time.Within each process:
tick(), compare the current tick counter to the scheduled trigger tick.This model avoids the unpredictability of real-time timers and makes Tickloom suitable for highly controlled distributed system testing.
In Tickloom, messages are defined as plain Java records.
This makes them:
Using plain records avoids the need for an external IDL or code generation step (such as Protocol Buffers or Thrift).
Instead, you can define messages directly in Java:
public record EchoRequest(String message) {}
public record EchoResponse(String message) {}
Serialization uses JSON by default, but the framework can be extended to support other formats.
Artifacts are on Maven Central under the io.github.unmeshjoshi group.
There is an examples github repo which demonstrates how to use tickloom as a library.
see tickloomexamples: https://github.com/unmeshjoshi/tickloomexamples
Gradle (Kotlin DSL)
dependencies {
implementation("io.github.unmeshjoshi:tickloom:0.1.0-alpha.7")
testImplementation("io.github.unmeshjoshi:tickloom-testkit:0.1.0-alpha.7")
}
Maven
<dependency>
<groupId>io.github.unmeshjoshi</groupId>
<artifactId>tickloom</artifactId>
<version>0.1.0-alpha.7</version>
</dependency>
<dependency>
<groupId>io.github.unmeshjoshi</groupId>
<artifactId>tickloom-testkit</artifactId>
<version>0.1.0-alpha.7</version>
<scope>test</scope>
</dependency>
Requirements: Java 21+
Compatibility
| TickLoom | tickloomexamples |
|-----------------|-------------------|
| 0.1.0-alpha.7 | 0.1.0-alpha.7 |
Below is a minimal Echo example that shows how to build on Tickloom’s primitives:
EchoServer extends ProcessEchoClient extends ClusterClientCluster testkit and assertEventually(Full code retained as in original)
// Example EchoServer and EchoClient code
public class EchoServer extends Process {
private final List<ProcessId> peerIds;
public EchoServer(ProcessId id,
List<ProcessId> peerIds,
MessageBus messageBus,
MessageCodec messageCodec,
Storage storage,
Clock clock,
int timeoutTicks) {
super(id, messageBus, messageCodec, timeoutTicks, clock);
this.peerIds = peerIds;
}
@Override
protected Map<MessageType, Handler> initialiseHandlers() {
return Map.of(
ECHO_REQUEST, this::onEchoRequest
);
}
private void onEchoRequest(Message msg) {
EchoRequest request = deserializePayload(msg.payload(), EchoRequest.class);
EchoResponse response = new EchoResponse(request.text());
Message responseMessage = createResponseMessage(msg, response, ECHO_RESPONSE);
try {
messageBus.sendMessage(responseMessage);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public class EchoClient extends ClusterClient {
public EchoClient(ProcessId clientId,
List<ProcessId> replicaEndpoints,
MessageBus messageBus,
MessageCodec messageCodec,
Clock clock,
int timeoutTicks) {
super(clientId, replicaEndpoints, messageBus, messageCodec, clock, timeoutTicks);
}
public ListenableFuture<EchoResponse> echo(ProcessId server, String text) {
EchoRequest req = new EchoRequest(text);
return sendRequest(req, server, ECHO_REQUEST);
}
@Override
protected java.util.Map<MessageType, Handler> initialiseHandlers() {
return java.util.Map.of(
ECHO_RESPONSE, msg -> {
EchoResponse resp = deserialize(msg.payload(), EchoResponse.class);
handleResponse(msg.correlationId(), resp, msg.source());
}
);
}
}
public class EchoClusterTest {
private Cluster cluster;
@BeforeEach
void setup() throws Exception {
cluster = new Cluster()
.withNumProcesses(1)
.useSimulatedNetwork()
.build(EchoServer::new)
.start();
}
@AfterEach
void teardown() {
if (cluster != null) cluster.close();
}
@Test
void echo_roundtrip() throws Exception {
ProcessId serverId = ProcessId.of("process-1");
EchoClient client = cluster.newClient(ProcessId.of("client-1"), (clientId, endpoints, bus, codec, clock, timeoutTicks) ->
new EchoClient(clientId, java.util.List.of(serverId), bus, codec, clock, timeoutTicks));
var future = client.echo(serverId, "hello");
assertEventually(cluster, () -> future.isCompleted());
assertEquals("hello", future.getResult().text());
}
}
The testkit (io.github.unmeshjoshi:tickloom-testkit) contains helpers to:
Example:
import com.tickloom.testkit.Cluster;
import com.tickloom.ProcessId;
Cluster cluster = new Cluster()
.withProcessIds(ProcessId.of("n1"), ProcessId.of("n2"), ProcessId.of("n3"))
.useSimulatedNetwork()
.withInitialClockTime(1)
.build((id, peers, bus, codec, storage, clock, timeout) -> /* create Replica */)
.start();
cluster.partitionNodes(ProcessId.of("n1"), ProcessId.of("n3"));
// Advance ticks until a condition is met
// assertEventually(cluster, () -> ...);
cluster.healPartition(ProcessId.of("n1"), ProcessId.of("n3"));
cluster.close();
TickLoom includes a simulation harness to drive repeatable workloads and verify correctness:
SimulationRunner: base class that runs a cluster for N ticks, issues client requests deterministically (by seed), and records a history.QuorumSimulationRunner: concrete runner for the quorum key-value example (issues GET/SET).Jepsen integration: converts history to EDN and checks linearizability using the Jepsen checker.long seed = 111_111L;
long ticks = 10_000L;
var runner = new com.tickloom.algorithms.replication.quorum.QuorumKVScenarioRunner(seed);
runner.runForTicks(ticks); // writes EDN to build/history_*.edn and runs Jepsen checker
Two runs with the same seed produce identical histories; different seeds generally differ.
var r1 = new QuorumSimulationRunner(42L);
var r2 = new QuorumSimulationRunner(42L);
var h1 = r1.runAndGetHistory(5_000);
var h2 = r2.runAndGetHistory(5_000);
assert h1.equals(h2); // same seed -> same history
See src/test/java/com/tickloom/SimulationRunnerTest.java for determinism tests.
SimulationRunner uses Jepsen to validate a register model. You can also call it directly:
var runner = new QuorumSimulationRunner(123L);
var history = runner.runAndGetHistory(5_000);
var consistencyChecker = new com.tickloom.ConsistencyChecker();
boolean ok = consistencyChecker.checkLinearizableRegister(history.toEdn());
System.out.println("Linearizable = " + ok);
TickLoom provides comprehensive consistency verification through both Jepsen integration and custom consistency checkers. This allows you to verify that your distributed algorithms maintain the correct consistency properties under various failure scenarios.
// Record operation history during simulation
var history = HistoryRecorder.newHistory();
// ... run your simulation, record ops ...
Path edn = history.writeEdn(Paths.get("build/history.edn"));
// Check linearizability using Jepsen
var resultLin = ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.REGISTER);
assert resultLin.valid();
// Check sequential consistency using custom checker
var resultSeq = ConsistencyChecker.check(edn, ConsistencyProperty.SEQUENTIAL_CONSISTENCY, DataModel.REGISTER);
assert resultSeq.valid();
// Register model (default)
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.REGISTER);
// Compare-and-swap register
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.CAS_REGISTER);
// Set operations
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.SET);
// Mutex operations
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.MUTEX);
// For multi-key operations (Jepsen independent checker)
boolean valid = ConsistencyChecker.checkIndependent(
edn,
ConsistencyProperty.LINEARIZABILITY,
DataModel.REGISTER
);
@Test
void testConsistencyUnderPartition() throws IOException {
try (var cluster = new Cluster()
.withProcessIds(List.of(ATHENS, BYZANTIUM, CYRENE, DELPHI, SPARTA))
.useSimulatedNetwork()
.build(QuorumReplica::new)
.start()) {
// Run operations with network partitions
cluster.partitionNodes(NodeGroup.of(ATHENS, BYZANTIUM), NodeGroup.of(CYRENE, DELPHI, SPARTA));
// Record history
History<String, String> history = new History<>();
// ... perform operations ...
// Verify consistency properties
String edn = history.toEdn();
boolean linearizable = ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.REGISTER);
boolean sequential = ConsistencyChecker.check(edn, ConsistencyProperty.SEQUENTIAL_CONSISTENCY, DataModel.REGISTER);
// Assertions based on expected behavior
assertFalse(linearizable, "Split-brain should violate linearizability");
assertTrue(sequential, "Should maintain sequential consistency");
}
}
Notes:
For more examples of using Jepsen, see src/test/java/com/tickloom/JepsenTest.java.
tick() loop)SimulatedNetwork: deterministic delivery with configurable delays, partitions, and lossNioNetwork: non-blocking networking for real deployments# Full build
./gradlew build
# Run tests
./gradlew test
Tickloom is for you if you:
LLMs are fascinating, but as we discussed in the article on conversational abstractions for LLMs, having stable abstractions helps us quickly build code by using them as vocabulary in prompts. With the primitive abstractions available in the TickLoom framework, I’ve found it relatively easy to quickly build example code for algorithms I want to try out. Here’s an example prompt:
Refer to TickLoom QuorumKVStoreTest code and create tests that demonstrate clock-skew scenarios where a minority partition with higher
timestamps can overwrite majority values after healing.
Use Jepsen history recording to prove the system violates
linearizability but maintains sequential consistency.
This prompt uses words like “TickLoom” and “Jepsen” but refers to a concrete implementation, making it easier for LLMs to have a strong context to work with. LLMs are very new, and everyone is experimenting with them—let’s continue and see what the future holds.
This project is licensed under the Apache License 2.0 – see the LICENSE file for details.
4 commits
Java
92.9%
Clojure
4.7%
Shell
1.5%
Tickloom is a lightweight Java framework for building deterministic, testable distributed systems. It implements patterns from Patterns of Distributed Systems and serves as a reference implementation for the Distributed Systems Patterns Workshop.
It gives you:
Distributed systems share common needs:
Tickloom provides all of these in a single-threaded deterministic model — making tests reproducible and easier to debug.
In Tickloom, tick() represents a single lock step of execution.
Each tick processes pending work in a fixed, deterministic order, ensuring reproducibility and predictable behavior across runs.
All components run in a single main thread — there are no worker threads.
This eliminates race conditions and makes behavior easier to reason about.
The core components implement Tickable and are invoked in sequence:
SimulatedNetwork decides delivery time based on configured delays, partitions, and packet loss. Messages are delivered only when their scheduled delivery tick is reached.NioNetwork processes available SelectionKeys from Java NIO’s selector in each tick.While network and storage operations are asynchronous in nature, Tickloom models them explicitly within the tick loop:
A driver calls tick() on all components in the defined order: Network → MessageBus → Process → Storage
This enables both realistic production behavior and reproducible simulation, which is easier to test.
Tickloom models time in terms of ticks, not real-world milliseconds.
Every call to tick() advances a logical tick counter by one.
This makes timing deterministic and reproducible across test runs and simulations.
Timeouts are configured in terms of the number of ticks before they expire — similar to the approach used in etcd and TigerBeetle:
5 ticks, and the current tick counter is 100, the timeout will trigger at tick 105.tick() in a loop, without waiting in real time.Within each process:
tick(), compare the current tick counter to the scheduled trigger tick.This model avoids the unpredictability of real-time timers and makes Tickloom suitable for highly controlled distributed system testing.
In Tickloom, messages are defined as plain Java records.
This makes them:
Using plain records avoids the need for an external IDL or code generation step (such as Protocol Buffers or Thrift).
Instead, you can define messages directly in Java:
public record EchoRequest(String message) {}
public record EchoResponse(String message) {}
Serialization uses JSON by default, but the framework can be extended to support other formats.
Artifacts are on Maven Central under the io.github.unmeshjoshi group.
There is an examples github repo which demonstrates how to use tickloom as a library.
see tickloomexamples: https://github.com/unmeshjoshi/tickloomexamples
Gradle (Kotlin DSL)
dependencies {
implementation("io.github.unmeshjoshi:tickloom:0.1.0-alpha.7")
testImplementation("io.github.unmeshjoshi:tickloom-testkit:0.1.0-alpha.7")
}
Maven
<dependency>
<groupId>io.github.unmeshjoshi</groupId>
<artifactId>tickloom</artifactId>
<version>0.1.0-alpha.7</version>
</dependency>
<dependency>
<groupId>io.github.unmeshjoshi</groupId>
<artifactId>tickloom-testkit</artifactId>
<version>0.1.0-alpha.7</version>
<scope>test</scope>
</dependency>
Requirements: Java 21+
Compatibility
| TickLoom | tickloomexamples |
|-----------------|-------------------|
| 0.1.0-alpha.7 | 0.1.0-alpha.7 |
Below is a minimal Echo example that shows how to build on Tickloom’s primitives:
EchoServer extends ProcessEchoClient extends ClusterClientCluster testkit and assertEventually(Full code retained as in original)
// Example EchoServer and EchoClient code
public class EchoServer extends Process {
private final List<ProcessId> peerIds;
public EchoServer(ProcessId id,
List<ProcessId> peerIds,
MessageBus messageBus,
MessageCodec messageCodec,
Storage storage,
Clock clock,
int timeoutTicks) {
super(id, messageBus, messageCodec, timeoutTicks, clock);
this.peerIds = peerIds;
}
@Override
protected Map<MessageType, Handler> initialiseHandlers() {
return Map.of(
ECHO_REQUEST, this::onEchoRequest
);
}
private void onEchoRequest(Message msg) {
EchoRequest request = deserializePayload(msg.payload(), EchoRequest.class);
EchoResponse response = new EchoResponse(request.text());
Message responseMessage = createResponseMessage(msg, response, ECHO_RESPONSE);
try {
messageBus.sendMessage(responseMessage);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public class EchoClient extends ClusterClient {
public EchoClient(ProcessId clientId,
List<ProcessId> replicaEndpoints,
MessageBus messageBus,
MessageCodec messageCodec,
Clock clock,
int timeoutTicks) {
super(clientId, replicaEndpoints, messageBus, messageCodec, clock, timeoutTicks);
}
public ListenableFuture<EchoResponse> echo(ProcessId server, String text) {
EchoRequest req = new EchoRequest(text);
return sendRequest(req, server, ECHO_REQUEST);
}
@Override
protected java.util.Map<MessageType, Handler> initialiseHandlers() {
return java.util.Map.of(
ECHO_RESPONSE, msg -> {
EchoResponse resp = deserialize(msg.payload(), EchoResponse.class);
handleResponse(msg.correlationId(), resp, msg.source());
}
);
}
}
public class EchoClusterTest {
private Cluster cluster;
@BeforeEach
void setup() throws Exception {
cluster = new Cluster()
.withNumProcesses(1)
.useSimulatedNetwork()
.build(EchoServer::new)
.start();
}
@AfterEach
void teardown() {
if (cluster != null) cluster.close();
}
@Test
void echo_roundtrip() throws Exception {
ProcessId serverId = ProcessId.of("process-1");
EchoClient client = cluster.newClient(ProcessId.of("client-1"), (clientId, endpoints, bus, codec, clock, timeoutTicks) ->
new EchoClient(clientId, java.util.List.of(serverId), bus, codec, clock, timeoutTicks));
var future = client.echo(serverId, "hello");
assertEventually(cluster, () -> future.isCompleted());
assertEquals("hello", future.getResult().text());
}
}
The testkit (io.github.unmeshjoshi:tickloom-testkit) contains helpers to:
Example:
import com.tickloom.testkit.Cluster;
import com.tickloom.ProcessId;
Cluster cluster = new Cluster()
.withProcessIds(ProcessId.of("n1"), ProcessId.of("n2"), ProcessId.of("n3"))
.useSimulatedNetwork()
.withInitialClockTime(1)
.build((id, peers, bus, codec, storage, clock, timeout) -> /* create Replica */)
.start();
cluster.partitionNodes(ProcessId.of("n1"), ProcessId.of("n3"));
// Advance ticks until a condition is met
// assertEventually(cluster, () -> ...);
cluster.healPartition(ProcessId.of("n1"), ProcessId.of("n3"));
cluster.close();
TickLoom includes a simulation harness to drive repeatable workloads and verify correctness:
SimulationRunner: base class that runs a cluster for N ticks, issues client requests deterministically (by seed), and records a history.QuorumSimulationRunner: concrete runner for the quorum key-value example (issues GET/SET).Jepsen integration: converts history to EDN and checks linearizability using the Jepsen checker.long seed = 111_111L;
long ticks = 10_000L;
var runner = new com.tickloom.algorithms.replication.quorum.QuorumKVScenarioRunner(seed);
runner.runForTicks(ticks); // writes EDN to build/history_*.edn and runs Jepsen checker
Two runs with the same seed produce identical histories; different seeds generally differ.
var r1 = new QuorumSimulationRunner(42L);
var r2 = new QuorumSimulationRunner(42L);
var h1 = r1.runAndGetHistory(5_000);
var h2 = r2.runAndGetHistory(5_000);
assert h1.equals(h2); // same seed -> same history
See src/test/java/com/tickloom/SimulationRunnerTest.java for determinism tests.
SimulationRunner uses Jepsen to validate a register model. You can also call it directly:
var runner = new QuorumSimulationRunner(123L);
var history = runner.runAndGetHistory(5_000);
var consistencyChecker = new com.tickloom.ConsistencyChecker();
boolean ok = consistencyChecker.checkLinearizableRegister(history.toEdn());
System.out.println("Linearizable = " + ok);
TickLoom provides comprehensive consistency verification through both Jepsen integration and custom consistency checkers. This allows you to verify that your distributed algorithms maintain the correct consistency properties under various failure scenarios.
// Record operation history during simulation
var history = HistoryRecorder.newHistory();
// ... run your simulation, record ops ...
Path edn = history.writeEdn(Paths.get("build/history.edn"));
// Check linearizability using Jepsen
var resultLin = ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.REGISTER);
assert resultLin.valid();
// Check sequential consistency using custom checker
var resultSeq = ConsistencyChecker.check(edn, ConsistencyProperty.SEQUENTIAL_CONSISTENCY, DataModel.REGISTER);
assert resultSeq.valid();
// Register model (default)
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.REGISTER);
// Compare-and-swap register
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.CAS_REGISTER);
// Set operations
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.SET);
// Mutex operations
ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.MUTEX);
// For multi-key operations (Jepsen independent checker)
boolean valid = ConsistencyChecker.checkIndependent(
edn,
ConsistencyProperty.LINEARIZABILITY,
DataModel.REGISTER
);
@Test
void testConsistencyUnderPartition() throws IOException {
try (var cluster = new Cluster()
.withProcessIds(List.of(ATHENS, BYZANTIUM, CYRENE, DELPHI, SPARTA))
.useSimulatedNetwork()
.build(QuorumReplica::new)
.start()) {
// Run operations with network partitions
cluster.partitionNodes(NodeGroup.of(ATHENS, BYZANTIUM), NodeGroup.of(CYRENE, DELPHI, SPARTA));
// Record history
History<String, String> history = new History<>();
// ... perform operations ...
// Verify consistency properties
String edn = history.toEdn();
boolean linearizable = ConsistencyChecker.check(edn, ConsistencyProperty.LINEARIZABILITY, DataModel.REGISTER);
boolean sequential = ConsistencyChecker.check(edn, ConsistencyProperty.SEQUENTIAL_CONSISTENCY, DataModel.REGISTER);
// Assertions based on expected behavior
assertFalse(linearizable, "Split-brain should violate linearizability");
assertTrue(sequential, "Should maintain sequential consistency");
}
}
Notes:
For more examples of using Jepsen, see src/test/java/com/tickloom/JepsenTest.java.
tick() loop)SimulatedNetwork: deterministic delivery with configurable delays, partitions, and lossNioNetwork: non-blocking networking for real deployments# Full build
./gradlew build
# Run tests
./gradlew test
Tickloom is for you if you:
LLMs are fascinating, but as we discussed in the article on conversational abstractions for LLMs, having stable abstractions helps us quickly build code by using them as vocabulary in prompts. With the primitive abstractions available in the TickLoom framework, I’ve found it relatively easy to quickly build example code for algorithms I want to try out. Here’s an example prompt:
Refer to TickLoom QuorumKVStoreTest code and create tests that demonstrate clock-skew scenarios where a minority partition with higher
timestamps can overwrite majority values after healing.
Use Jepsen history recording to prove the system violates
linearizability but maintains sequential consistency.
This prompt uses words like “TickLoom” and “Jepsen” but refers to a concrete implementation, making it easier for LLMs to have a strong context to work with. LLMs are very new, and everyone is experimenting with them—let’s continue and see what the future holds.
This project is licensed under the Apache License 2.0 – see the LICENSE file for details.
4 commits
Java
92.9%
Clojure
4.7%
Shell
1.5%