Skip to content

Java vs Rust Benchmark — 10M Transactions

This page compares Spring Batch (Java 25 / Spring Boot 4.x) and Spring Batch RS (Rust) on a realistic ETL pipeline: reading 10 million financial transactions from CSV, storing them in PostgreSQL, then exporting to XML.

Both implementations use identical settings — chunk size 1 000, connection pool 10, same data schema — so the comparison is apples-to-apples.


ParameterValue
Machine8-core CPU, 16 GB RAM, NVMe SSD
OSUbuntu 22.04 LTS
PostgreSQL15.4 (local, same machine)
JavaOpenJDK 25, Spring Boot 4.0.3, Spring Batch 6.x
JVM flags-Xms512m -Xmx4g -XX:+UseG1GC + virtual threads enabled
Rust1.77 stable, --release (opt-level = 3)
JVM GCG1GC, logged with -Xlog:gc*:gc.log
Virtual threadsEnabled (spring.threads.virtual.enabled=true)
Chunk size1 000 (both)
Pool size10 connections (both)

transactions.csv (10M rows)
▼ CsvItemReader / FlatFileItemReader
TransactionProcessor
(USD/GBP → EUR conversion, CANCELLED → FAILED)
▼ PostgresItemWriter / JdbcBatchItemWriter (bulk insert, chunk=1000)
PostgreSQL: table transactions
▼ RdbcItemReader / JdbcPagingItemReader (paginated, page_size=1000)
▼ XmlItemWriter / StaxEventItemWriter
transactions_export.xml
FieldTypeExample
transaction_idstringTXN-0000000001
amountfloat1234.56
currencystringUSD, EUR, GBP
timestampstring2024-06-15T12:00:00Z
account_fromstringACC-00042137
account_tostringACC-00891023
statusstringPENDING, COMPLETED, FAILED, CANCELLED
amount_eurfloat1135.80 (added by processor)

#[derive(Debug, Clone, Deserialize, Serialize, FromRow)]
struct Transaction {
transaction_id: String,
amount: f64,
currency: String,
timestamp: String,
account_from: String,
account_to: String,
status: String,
#[serde(default)]
amount_eur: f64,
}

Processor (currency conversion + status normalisation)

Section titled “Processor (currency conversion + status normalisation)”
#[derive(Default)]
struct TransactionProcessor;
impl ItemProcessor<Transaction, Transaction> for TransactionProcessor {
fn process(&self, item: &Transaction) -> ItemProcessorResult<Transaction> {
let rate = match item.currency.as_str() {
"USD" => 0.92,
"GBP" => 1.17,
_ => 1.0,
};
let status = if item.status == "CANCELLED" {
"FAILED".to_string()
} else {
item.status.clone()
};
Ok(Some(Transaction {
amount_eur: (item.amount * rate * 100.0).round() / 100.0,
status,
..item.clone()
})
}
}

let file = File::open(csv_path)?;
let buffered = BufReader::with_capacity(64 * 1024, file);
let reader = CsvItemReaderBuilder::<Transaction>::new()
.has_headers(true)
.from_reader(buffered);
let writer = RdbcItemWriterBuilder::<Transaction>::new()
.postgres(&pool)
.table("transactions")
.add_column("transaction_id")
// ... 8 columns total
.postgres_binder(&TransactionBinder)
.build_postgres();
let step = StepBuilder::new("csv-to-postgres")
.chunk::<Transaction, Transaction>(1_000)
.reader(&reader)
.processor(&TransactionProcessor)
.writer(&writer)
.build();

let reader = RdbcItemReaderBuilder::<Transaction>::new()
.postgres(pool.clone())
.query(
"SELECT transaction_id, amount, currency, timestamp, \
account_from, account_to, status, amount_eur \
FROM transactions ORDER BY transaction_id",
)
.with_page_size(1_000)
.build_postgres();
let writer = XmlItemWriterBuilder::<Transaction>::new()
.root_tag("transactions")
.item_tag("transaction")
.from_path(xml_path)?;
let step = StepBuilder::new("postgres-to-xml")
.chunk::<Transaction, Transaction>(1_000)
.reader(&reader)
.processor(&PassThroughProcessor::new())
.writer(&writer)
.build();

Measured on the reference environment described above.

MetricSpring Batch RS (Rust)Spring Batch (Java)Rust advantage
Total pipeline time42 s187 s4.5× faster
Step 1 duration (CSV→PG)28 s124 s4.4×
Step 2 duration (PG→XML)14 s63 s4.5×
JVM / binary startup< 10 ms3 200 ms320×
Deployable artefact size8 MB (binary)47 MB (fat JAR)6× smaller
StepRustJavaRatio
Step 1 — CSV → PostgreSQL357 00080 6004.4×
Step 2 — PostgreSQL → XML714 000158 7004.5×
MetricRustJava
Peak RSS62 MB1 840 MB
Heap peakN/A (no GC)1 620 MB
Steady-state RSS~45 MB~820 MB
MetricValue
Total GC events312
Total GC pause time8.4 s
Longest single pause340 ms
% of runtime in GC4.5%

1. No garbage collection. Java’s G1GC paused for a cumulative 8.4 seconds. Rust uses RAII — memory is freed the instant a chunk goes out of scope, with zero overhead and zero latency spikes.

2. Lower memory pressure. Java holds JVM metadata, class bytecode, and JIT-compiled code in addition to heap data. Spring Batch also retains JobExecution and StepExecution objects throughout the run. Rust’s binary is a single executable: 62 MB vs 1 840 MB peak RSS.

3. Zero-cost abstractions. Rust’s trait-based pipeline (ItemReaderItemProcessorItemWriter) compiles to a tight loop with no virtual dispatch overhead. Java’s pipeline involves Spring AOP, proxy objects, and transaction management wrappers on every chunk boundary.

4. Startup time. The JVM takes 3.2 s to start, load classes, and JIT-compile hot paths. The Rust binary starts in under 10 ms — critical for short jobs or frequent schedules.

  • Your team is Java-first and migration cost outweighs performance gains
  • You need Spring ecosystem integrations (Spring Data, Spring Cloud Task, Spring Integration)
  • Your batch jobs run infrequently and throughput is not the bottleneck
  • You require rich operational features: JobRepository, JobExplorer, REST API control
  • Throughput and latency are business requirements (financial settlement, real-time ETL)
  • Memory is constrained (embedded systems, small containers)
  • GC pauses would cause SLA violations
  • You want a single statically-linked binary with no runtime dependency
  • Cold-start time matters (serverless, frequent scheduling)

Terminal window
# PostgreSQL 15+ (Docker):
docker run -d --name pg-bench \
-p 5432:5432 \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=benchmark \
postgres:15
Terminal window
# Build in release mode (required for fair comparison)
cargo build --release --example benchmark_csv_postgres_xml \
--features csv,xml,rdbc-postgres
# Run and measure peak RSS
/usr/bin/time -v \
cargo run --release --example benchmark_csv_postgres_xml \
--features csv,xml,rdbc-postgres \
2>&1 | tee rust_bench.log
# Extract key metrics
grep -E "Step|SUMMARY|Maximum resident" rust_bench.log
Terminal window
cd benchmark/java
# Requires Java 25 + Maven 3.9+
# Build fat JAR (Spring Boot 4.0.3 / Spring Batch 6.x)
mvn package -q -DskipTests
# Run with GC logging, virtual threads, and RSS measurement
/usr/bin/time -v java \
-Xms512m -Xmx4g \
-XX:+UseG1GC \
-Xlog:gc*:gc.log \
-jar target/spring-batch-benchmark-1.0.0.jar \
--spring.datasource.url=jdbc:postgresql://localhost:5432/benchmark \
2>&1 | tee java_bench.log
# Parse GC summary
grep "Pause" gc.log | tail -20
grep "Maximum resident" java_bench.log