Context
The EvmTransactionBatchService buffers incoming EVM transactions and flushes them in batches to the EvmTransactionProcessor. It uses the same dual-trigger mechanism as the existing Solana TransactionBatchService: flush when either the batch reaches a size threshold (200 transactions) or a time threshold (100ms) elapses, whichever comes first. This batching strategy reduces database round-trips while maintaining low latency. The unbounded LinkedTransferQueue prevents gRPC/WebSocket backpressure disconnection.
Specification
File
prism/src/main/java/com/stablebridge/prism/domain/service/EvmTransactionBatchService.java
Constructor dependencies
EvmTransactionProcessor — the delegate for batch processing
- Batch size:
200 (constant)
- Batch timeout:
100ms (constant)
Implementation pattern
Follow TransactionBatchService.java exactly:
@Singleton
@RequiredArgsConstructor
@Slf4j
public class EvmTransactionBatchService implements Lifecycle {
private static final int BATCH_SIZE = 200;
private static final Duration BATCH_TIMEOUT = Duration.ofMillis(100);
private final EvmTransactionProcessor processor;
private final LinkedTransferQueue<EvmTransactionWithBlock> queue = new LinkedTransferQueue<>();
private volatile boolean running;
private Thread drainThread;
public void enqueue(EvmTransaction transaction, EvmBlock block) {
queue.add(new EvmTransactionWithBlock(transaction, block));
}
@Override
public void start() {
running = true;
drainThread = Thread.ofVirtual().name("evm-tx-batch-drain").start(this::drainLoop);
}
@Override
public void stop() {
running = false;
if (drainThread != null) drainThread.interrupt();
}
private void drainLoop() {
var batch = new ArrayList<EvmTransactionWithBlock>(BATCH_SIZE);
while (running) {
try {
// Wait for first item with timeout
var first = queue.poll(BATCH_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
if (first != null) {
batch.add(first);
queue.drainTo(batch, BATCH_SIZE - 1);
}
if (!batch.isEmpty()) {
flush(batch);
batch.clear();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// Final drain on shutdown
queue.drainTo(batch);
if (!batch.isEmpty()) flush(batch);
}
private void flush(List<EvmTransactionWithBlock> batch) {
// Group by block, delegate to processor
// ...
}
private record EvmTransactionWithBlock(EvmTransaction transaction, EvmBlock block) {}
}
Design decisions
LinkedTransferQueue (unbounded) — same as Solana batch service, prevents backpressure disconnection
- Dual-trigger: 200 tx OR 100ms, whichever fires first
- Virtual thread for the drain loop (
Thread.ofVirtual())
- Implements
Lifecycle interface for clean start/stop
- Final drain on shutdown ensures no data loss
- Groups transactions by block before delegating to processor (since processor needs the block)
volatile boolean running for thread-safe stop signal
@Singleton (Avaje DI), @RequiredArgsConstructor (Lombok), @Slf4j (logging)
Test class
prism/src/test/java/com/stablebridge/prism/domain/service/EvmTransactionBatchServiceTest.java
Test cases:
- Single enqueued transaction is flushed after timeout
- Batch of 200 transactions is flushed immediately (size trigger)
- Mixed enqueue rates trigger both size and time flushes
- Stop drains remaining items
- Empty queue does not trigger processor
- Processor is called with correct transaction-block grouping
Acceptance Criteria
Dependencies
References
Context
The
EvmTransactionBatchServicebuffers incoming EVM transactions and flushes them in batches to theEvmTransactionProcessor. It uses the same dual-trigger mechanism as the existing SolanaTransactionBatchService: flush when either the batch reaches a size threshold (200 transactions) or a time threshold (100ms) elapses, whichever comes first. This batching strategy reduces database round-trips while maintaining low latency. The unboundedLinkedTransferQueueprevents gRPC/WebSocket backpressure disconnection.Specification
File
prism/src/main/java/com/stablebridge/prism/domain/service/EvmTransactionBatchService.javaConstructor dependencies
EvmTransactionProcessor— the delegate for batch processing200(constant)100ms(constant)Implementation pattern
Follow
TransactionBatchService.javaexactly:Design decisions
LinkedTransferQueue(unbounded) — same as Solana batch service, prevents backpressure disconnectionThread.ofVirtual())Lifecycleinterface for clean start/stopvolatile boolean runningfor thread-safe stop signal@Singleton(Avaje DI),@RequiredArgsConstructor(Lombok),@Slf4j(logging)Test class
prism/src/test/java/com/stablebridge/prism/domain/service/EvmTransactionBatchServiceTest.javaTest cases:
Acceptance Criteria
EvmTransactionBatchServiceexists withLinkedTransferQueueand dual-trigger batchingLifecyclewith clean start/stopTransactionBatchServicepattern exactly./gradlew buildpassesDependencies
References