Skip to content

Instantly share code, notes, and snippets.

@muhdkhokhar
Created November 20, 2025 15:51
Show Gist options
  • Select an option

  • Save muhdkhokhar/95690ea6bb86c8ba0f3c0128b17578b0 to your computer and use it in GitHub Desktop.

Select an option

Save muhdkhokhar/95690ea6bb86c8ba0f3c0128b17578b0 to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>chronicle-event-store</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<chronicle.version>2.25.105</chronicle.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.openhft</groupId>
<artifactId>chronicle-queue</artifactId>
<version>${chronicle.version}</version>
</dependency>
<dependency>
<groupId>net.openhft</groupId>
<artifactId>chronicle-map</artifactId>
<version>${chronicle.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.example.chronicleeventstore;
import net.openhft.chronicle.wire.SelfDescribingMarshallable;
public class Event extends SelfDescribingMarshallable {
public String eventId; // The main event ID (e.g., "001")
public int stepNumber; // The sequence number
public long timestamp;
public String stepName; // e.g., "getdata"
public String stepOutput; // The rich output (JSON, table data, etc.)
// Standard getters/setters/constructors omitted for brevity, but needed in a real project
// Chronicle only requires public fields or standard Java bean conventions
}
package com.example.chronicleeventstore;
import net.openhft.chronicle.map.ChronicleMap;
import net.openhft.chronicle.queue.ChronicleQueue;
import net.openhft.chronicle.queue.ExcerptAppender;
import net.openhft.chronicle.queue.ExcerptTailer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
public class EventStorageService {
@Value("${chronicle.queue.path:/tmp/chronicle-events}")
private String queuePath;
// Chronicle Components
private ChronicleQueue queue;
private ExcerptAppender appender;
private ChronicleMap<String, List<Long>> eventIndexMap;
@PostConstruct
public void init() throws IOException {
// 1. Initialize Chronicle Queue
File queueDir = new File(queuePath);
queueDir.mkdirs();
this.queue = ChronicleQueue.single(queueDir.getAbsolutePath());
this.appender = queue.acquireAppender();
// 2. Initialize Chronicle Map (Event Index)
// Key: Event ID (String), Value: List of Queue Indices (Longs)
this.eventIndexMap = ChronicleMap
.of(String.class, (Class<List<Long>>) (Class<?>) List.class)
.name("event-index-map")
.averageKey("event-001")
.averageValue(List.of(0L, 0L, 0L, 0L)) // Estimate for 4 steps per event
.entries(10_000)
.create();
System.out.println("Chronicle initialized. Queue at: " + queuePath);
}
// --- WRITE/INDEX METHOD ---
public void saveStep(Event event) {
// 1. Write the Step to Chronicle Queue
appender.writeDocument(w -> w.writeMarshallable(event));
// 2. Capture the index (address) of the written entry
long index = appender.lastIndex();
// 3. Update the Chronicle Map index
// Use computeIfAbsent for a thread-safe way to get/create the list
eventIndexMap.compute(event.eventId, (id, indices) -> {
if (indices == null) {
indices = new ArrayList<>();
}
indices.add(index);
return indices;
});
System.out.printf("-> Wrote step %d for Event %s at index %d%n", event.stepNumber, event.eventId, index);
}
// --- READ/LOOKUP METHOD ---
public List<Event> getEventHistory(String eventId) {
// 1. Map Lookup: Get the list of indices for the Event ID
List<Long> indices = eventIndexMap.get(eventId);
if (indices == null || indices.isEmpty()) {
return List.of();
}
List<Event> history = new ArrayList<>();
ExcerptTailer tailer = queue.createTailer();
// 2. Queue Lookup: Jump directly to each index and read the data
for (Long index : indices) {
boolean success = tailer.moveToIndex(index);
if (success) {
Event event = tailer.readDocument(r -> r.readMarshallable(Event.class));
if (event != null) {
history.add(event);
}
}
}
return history;
}
@PreDestroy
public void close() {
// Ensure resources are closed cleanly
if (eventIndexMap != null) {
eventIndexMap.close();
}
if (queue != null) {
queue.close();
}
System.out.println("Chronicle resources closed.");
}
}
package com.example.chronicleeventstore;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/events")
public class EventController {
private final EventStorageService storageService;
public EventController(EventStorageService storageService) {
this.storageService = storageService;
}
@PostMapping("/simulate")
public ResponseEntity<String> simulateEvent() {
String newEventId = "EVENT-" + UUID.randomUUID().toString().substring(0, 4);
// --- Simulate Step 1: getData ---
Event step1 = new Event();
step1.eventId = newEventId;
step1.stepNumber = 1;
step1.timestamp = System.currentTimeMillis();
step1.stepName = "getdata";
step1.stepOutput = "{\"query\":\"SELECT * FROM users;\", \"rows\":15}";
storageService.saveStep(step1);
// --- Simulate Step 2: transform message ---
Event step2 = new Event();
step2.eventId = newEventId;
step2.stepNumber = 2;
step2.timestamp = System.currentTimeMillis() + 50;
step2.stepName = "transform message";
step2.stepOutput = "{\"status\":\"processed\",\"user_count\":15}";
storageService.saveStep(step2);
// --- Simulate Step 3: send message to Kafka ---
Event step3 = new Event();
step3.eventId = newEventId;
step3.stepNumber = 3;
step3.timestamp = System.currentTimeMillis() + 100;
step3.stepName = "send message to Kafka";
step3.stepOutput = "topic: processed_events, partition: 2, offset: 12345";
storageService.saveStep(step3);
return ResponseEntity.ok("Simulated new event: **" + newEventId + "** with 3 steps.");
}
@GetMapping("/{eventId}")
public List<Event> getEventHistory(@PathVariable String eventId) {
System.out.println("<- UI Request for Event ID: " + eventId);
// This simulates the timeline request: Map lookup -> Queue lookup
return storageService.getEventHistory(eventId);
}
}
package com.example.chronicleeventstore;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ChronicleApp {
public static void main(String[] args) {
SpringApplication.run(ChronicleApp.class, args);
}
}
package com.example.chronicleeventstore;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/events")
// Add CORS support so Angular running on port 4200 can connect
@CrossOrigin(origins = "http://localhost:4200")
public class EventController {
private final EventStorageService storageService;
public EventController(EventStorageService storageService) {
this.storageService = storageService;
}
// ... (simulateEvent method is the same)
@PostMapping("/simulate")
public ResponseEntity<String> simulateEvent() {
String newEventId = "EVENT-" + UUID.randomUUID().toString().substring(0, 4);
// --- Simulate Step 1: getData ---
Event step1 = new Event();
step1.eventId = newEventId;
step1.stepNumber = 1;
step1.timestamp = System.currentTimeMillis();
step1.stepName = "getdata";
step1.stepOutput = "{\"query\":\"SELECT * FROM users;\", \"rows\":15}";
storageService.saveStep(step1);
// --- Simulate Step 2: transform message ---
Event step2 = new Event();
step2.eventId = newEventId;
step2.stepNumber = 2;
step2.timestamp = System.currentTimeMillis() + 50;
step2.stepName = "transform message";
step2.stepOutput = "{\"status\":\"processed\",\"user_count\":15}";
storageService.saveStep(step2);
// --- Simulate Step 3: send message to Kafka ---
Event step3 = new Event();
step3.eventId = newEventId;
step3.stepNumber = 3;
step3.timestamp = System.currentTimeMillis() + 100;
step3.stepName = "send message to Kafka";
step3.stepOutput = "topic: processed_events, partition: 2, offset: 12345";
storageService.saveStep(step3);
return ResponseEntity.ok(newEventId); // Return just the ID for the UI
}
// ... (getEventHistory method is the same)
@GetMapping("/{eventId}")
public List<Event> getEventHistory(@PathVariable String eventId) {
System.out.println("<- UI Request for Event ID: " + eventId);
return storageService.getEventHistory(eventId);
}
}
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Event } from '../event.model';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-timeline',
templateUrl: './timeline.component.html',
styleUrls: ['./timeline.component.css']
})
export class TimelineComponent {
eventId: string = '';
eventHistory: Event[] = [];
isLoading: boolean = false;
errorMessage: string = '';
apiBaseUrl: string = 'http://localhost:8080/api/events';
constructor(private http: HttpClient) { }
/**
* Simulates a new event by calling the backend's POST endpoint.
*/
simulateEvent() {
this.isLoading = true;
this.errorMessage = '';
this.eventHistory = [];
this.http.post(this.apiBaseUrl + '/simulate', {}, { responseType: 'text' })
.subscribe({
next: (id: string) => {
this.eventId = id;
console.log(`Simulated Event ID: ${id}`);
// Automatically fetch the history of the new event
this.fetchHistory();
},
error: (err) => {
this.errorMessage = 'Failed to simulate event. Is the Spring Boot backend running?';
this.isLoading = false;
}
});
}
/**
* Fetches the event history for the entered Event ID.
*/
fetchHistory() {
if (!this.eventId) {
this.errorMessage = 'Please enter an Event ID or simulate a new event.';
return;
}
this.isLoading = true;
this.errorMessage = '';
this.eventHistory = [];
this.http.get<Event[]>(`${this.apiBaseUrl}/${this.eventId}`)
.pipe(
// Map the timestamp to a readable format for display
map(events => events.map(event => ({
...event,
timestamp: new Date(event.timestamp).toLocaleString() as any // Overriding type for display
})))
)
.subscribe({
next: (history) => {
this.eventHistory = history;
this.isLoading = false;
if (history.length === 0) {
this.errorMessage = `No history found for Event ID: ${this.eventId}`;
}
},
error: (err) => {
this.errorMessage = 'Error fetching history. Check console for details.';
this.isLoading = false;
}
});
}
}
That's a great next step! To give you a complete, runnable example, I'll provide a structure for the backend (Spring Boot/Chronicle) and a simple frontend (Angular) that consumes the data and renders a basic timeline.
Since I cannot directly create a ZIP file, I will provide the files organized into their respective folders. You can copy these files into a project structure and run them.
📦 Project Structure
chronicle-event-store-project/
├── backend/
│ ├── src/main/java/com/example/chronicleeventstore/
│ │ ├── ChronicleApp.java (Main Spring Boot App)
│ │ ├── Event.java (The Step Data Model)
│ │ ├── EventStorageService.java (CQ/CM Logic)
│ │ └── EventController.java (REST API)
│ └── pom.xml
└── frontend/
├── src/
│ ├── app/
│ │ ├── timeline/
│ │ │ ├── timeline.component.ts
│ │ │ ├── timeline.component.html
│ │ │ └── timeline.component.css
│ │ └── app.component.html
│ └── assets/
└── package.json
1. ☕ Backend (Spring Boot/Chronicle)
The backend code remains largely the same as the previous response, but I've added a configuration to allow the Angular UI to connect (CORS).
backend/src/main/java/.../EventController.java (Updated with CORS)
Java
package com.example.chronicleeventstore;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/events")
// Add CORS support so Angular running on port 4200 can connect
@CrossOrigin(origins = "http://localhost:4200")
public class EventController {
private final EventStorageService storageService;
public EventController(EventStorageService storageService) {
this.storageService = storageService;
}
// ... (simulateEvent method is the same)
@PostMapping("/simulate")
public ResponseEntity<String> simulateEvent() {
String newEventId = "EVENT-" + UUID.randomUUID().toString().substring(0, 4);
// --- Simulate Step 1: getData ---
Event step1 = new Event();
step1.eventId = newEventId;
step1.stepNumber = 1;
step1.timestamp = System.currentTimeMillis();
step1.stepName = "getdata";
step1.stepOutput = "{\"query\":\"SELECT * FROM users;\", \"rows\":15}";
storageService.saveStep(step1);
// --- Simulate Step 2: transform message ---
Event step2 = new Event();
step2.eventId = newEventId;
step2.stepNumber = 2;
step2.timestamp = System.currentTimeMillis() + 50;
step2.stepName = "transform message";
step2.stepOutput = "{\"status\":\"processed\",\"user_count\":15}";
storageService.saveStep(step2);
// --- Simulate Step 3: send message to Kafka ---
Event step3 = new Event();
step3.eventId = newEventId;
step3.stepNumber = 3;
step3.timestamp = System.currentTimeMillis() + 100;
step3.stepName = "send message to Kafka";
step3.stepOutput = "topic: processed_events, partition: 2, offset: 12345";
storageService.saveStep(step3);
return ResponseEntity.ok(newEventId); // Return just the ID for the UI
}
// ... (getEventHistory method is the same)
@GetMapping("/{eventId}")
public List<Event> getEventHistory(@PathVariable String eventId) {
System.out.println("<- UI Request for Event ID: " + eventId);
return storageService.getEventHistory(eventId);
}
}
The other backend files (ChronicleApp.java, Event.java, EventStorageService.java, and pom.xml) are identical to the previous response.
2. 🎨 Frontend (Angular)
This is a minimal Angular application that allows a user to input an Event ID and displays the result as a simple timeline.
Setup Instructions (Assuming you have Node.js and Angular CLI installed)
Navigate into the frontend directory.
Run npm install to install dependencies.
Run ng serve --open to start the application (it usually runs on http://localhost:4200).
frontend/src/app/event.model.ts
<div class="container">
<h2>Chronicle Event Timeline Viewer</h2>
<div class="input-group">
<input
type="text"
[(ngModel)]="eventId"
placeholder="Enter Event ID (e.g., EVENT-a8c3)">
<button (click)="fetchHistory()" [disabled]="isLoading">
{{ isLoading && eventId ? 'Loading...' : 'Load History' }}
</button>
<button (click)="simulateEvent()" [disabled]="isLoading">
{{ isLoading && !eventId ? 'Simulating...' : 'Simulate New Event' }}
</button>
</div>
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
<div *ngIf="eventHistory.length > 0">
<h3>Timeline for Event: <strong>{{ eventHistory[0].eventId }}</strong></h3>
<div class="timeline">
<div *ngFor="let step of eventHistory" class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4>Step {{ step.stepNumber }}: {{ step.stepName | titlecase }}</h4>
<p class="timestamp">
<small>Completed at: {{ step.timestamp }}</small>
</p>
<div class="output-panel">
<strong>Step Output:</strong>
<pre>{{ step.stepOutput }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
.container {
max-width: 800px;
margin: 40px auto;
font-family: Arial, sans-serif;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-group input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.input-group button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.timeline {
position: relative;
padding: 20px 0;
margin-left: 20px;
}
/* Vertical line */
.timeline::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
background-color: #e0e0e0;
}
.timeline-item {
position: relative;
padding-left: 30px;
margin-bottom: 30px;
}
.timeline-dot {
position: absolute;
top: 5px;
left: -8px;
width: 16px;
height: 16px;
background-color: #007bff;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 0 0 2px #007bff;
}
.timeline-content {
background: #f9f9f9;
padding: 15px;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.timestamp {
color: #888;
margin-top: 5px;
}
.output-panel {
margin-top: 10px;
padding: 10px;
background-color: #eee;
border-radius: 4px;
}
.output-panel pre {
white-space: pre-wrap;
word-break: break-all;
font-size: 0.9em;
margin: 0;
}
.error-message {
padding: 10px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin-bottom: 20px;
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // Required for ngModel
import { HttpClientModule } from '@angular/common/http'; // Required for API calls
import { AppComponent } from './app.component';
import { TimelineComponent } from './timeline/timeline.component';
@NgModule({
declarations: [
AppComponent,
TimelineComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment