Yesterday the DuckDB team published a preview of version 2.0, codename Cyanoptera, and it immediately shot to the top of Hacker News with over 650 points. Buried in that announcement is a line that should matter to every Java developer who has been ignoring DuckDB because "it is just an embedded analytics thing": DuckDB is getting a client/server mode.
The same database that runs inside your JVM as a single jar is about to serve queries over the network to other processes. Combined with a new VARIANT type, a rewritten SQL parser, and a storage format bump, v2.0 is the biggest release in the project's history, built from more than 10,000 commits since v1.5 shipped in March.
I have spent six years building Spring Boot services in Dhaka, mostly on PostgreSQL, and I run my own small AI agent infrastructure on the side. Analytical queries were always the awkward part: you either hammer your OLTP database with aggregates it hates, or you stand up a whole extra data platform for a handful of reports. An embedded analytical engine that speaks JDBC fixes exactly that. So this week I wired DuckDB into a Spring Boot service to see how it feels, and to figure out what v2.0 changes for Java teams. Here is the practical rundown.
What DuckDB v2.0 actually announces
The announcement is a preview, not the release itself. Version 2.0 ships "this fall." But the headline features are locked in enough to plan around:
-
DuckDB as a server. The new
quackextension implements DuckDB's native network protocol. Any DuckDB process can serve its databases, and any other DuckDB can attach and route queries to it. The team calls it "the year of DuckDB as a server." The extension graduates to stable in v2.0. - The CONNECT statement. Instead of pulling whole tables over the wire, a session can point itself at a remote database and push SQL down. This is not DuckDB-only: the new remote pushdown optimizer sends queries straight to PostgreSQL and MySQL servers too.
-
VARIANT becomes first-class. VARIANT, shipped in v1.5 as "JSON on steroids," stores differently-shaped data per row, auto-detects common structure, and shreds it so it compresses well and queries fast. In v2.0 the pipeline works end to end, including shredded reading and writing for Parquet, plus a family of
variant_*functions. - Massive speedups. The recursive CTE engine was rewritten. On a one-million-edge graph reachability query, run time drops from 4.90 seconds on v1.5.4 to 0.12 seconds on the v2.0 preview. That is roughly 40x on the same query. Timezone and collation work moved off ICU onto a native implementation, cutting 25 million timestamp conversions from 0.24 s to 0.11 s and German-collation filtering from 0.15 s to 0.06 s.
-
A new SQL parser. DuckDB is finally leaving the PostgreSQL-derived parser behind for its own PEG-based one, with better error messages, extension hooks into the grammar itself, and the first dialect compatibility mode (
SET dialect_compatibility_mode = 'spark'). - A new default storage format. Version 2.0.0, with lazily loaded column metadata, FSST string dictionary compression on by default, compact deletes, and stronger corruption validation. Databases with wide tables open faster and use less memory.
- A stable C API for extensions. Extensions can be written once, built once, and keep working across DuckDB versions. Organizations can also host their own signed extension repositories.
One more detail that caught my eye: the team points out that DuckDB has been a transactional, multi-connection database with full MVCC since day one. Most people never noticed because single-process usage never exercised it. The client/server mode is what finally puts that machinery to work.
Why this matters if you write Java for a living
Here is the uncomfortable truth about analytics in typical Spring Boot services. Your Postgres instance is excellent at what it was built for: many small transactions, strong consistency, high concurrency. Then somebody needs "monthly revenue by city, last 24 months, with cohort retention," and that same database groans while the connection pool starves.
The usual escape hatches are heavy. A separate Postgres read replica still executes aggregates row by row. A data warehouse means ClickHouse or BigQuery contracts, ingestion pipelines, and a new operational surface. For a team of five, that is a lot of ceremony for a dashboard.
DuckDB attacks this from the other end. It is columnar, vectorized, and analytical, but it embeds in your application like SQLite. In a Spring Boot service that means: add one Maven dependency, query Parquet files straight from S3 or local disk with plain SQL, and skip building a pipeline at all. The official Java JDBC client is a primary client, meaning it is first in line for new features and covered by community support.
And now v2.0 removes the two remaining objections:
- "Embedded means single process." With quack going stable, a Spring Boot service can expose its DuckDB database to other services, or attach to a shared DuckDB server, using token-authenticated connections. Your reporting service and your ingestion service no longer need to be the same JVM.
- "It cannot do real transactions." It always could, with MVCC. The server mode plus the new observability work, metrics, and logging makes long-running multi-tenant deployments realistic.
To be clear about what I have and have not done: I wired DuckDB into a Spring Boot service using the current stable JDBC driver, version 1.5.5.1 on Maven Central. I have not run the v2.0 preview build in that service, and the JDBC driver for v2.0 will land when the release does. The tutorial below is the stable, production-usable pattern today, and the v2.0 notes tell you what to revisit in the fall.
Tutorial: DuckDB inside a Spring Boot service
The scenario: your service receives event data as Parquet files (exports, logs, whatever your pipeline dumps), and management wants aggregations without loading everything into Postgres.
Step 1: Add the dependency
In your pom.xml:
<dependency>
<groupId>org.duckdb</groupId>
<artifactId>duckdb_jdbc</artifactId>
<version>1.5.5.1</version>
</dependency>
That single artifact bundles the native DuckDB library for your platform. No server to install, no daemon to babysit.
Step 2: Configure the DataSource
DuckDB is in-process, so connection handling is different from Postgres. The jdbc:duckdb: URL alone creates an in-memory database that dies with your JVM. For anything persistent, point at a file:
spring:
datasource:
url: jdbc:duckdb:/var/data/analytics.duckdb
driver-class-name: org.duckdb.Driver
With that, JdbcTemplate works out of the box, because to Spring this is just another JDBC DataSource.
Two things to know before you pool aggressively:
- Multiple connections to the same database file inside one process are supported. You can use HikariCP with a modest pool size.
- Across processes, the model is one writer or many readers. Multiple Java processes can read the same database file simultaneously in read-only mode by setting the connection property
duckdb.read_onlytotrue. Mixing a read-write connection with read-only ones on the same file is unsupported. Plan your deployment topology around that: one service owns writes, others attach read-only.
For a more efficient second connection inside the same JVM, the driver offers DuckDBConnection#duplicate() instead of going through DriverManager again.
Step 3: Query Parquet with plain SQL
This is the part that feels illegal the first time you do it. DuckDB reads Parquet directly, including globs, no loading step:
@Service
public class EventAnalyticsService {
private final JdbcTemplate jdbc;
public EventAnalyticsService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public List<CityRevenue> revenueByCity(String month) {
String sql = """
SELECT city, sum(amount) AS total
FROM read_parquet('/var/data/events/%s/*.parquet')
GROUP BY city
ORDER BY total DESC
""".formatted(month);
return jdbc.query(sql, (rs, i) ->
new CityRevenue(rs.getString("city"), rs.getBigDecimal("total")));
}
}
No staging table. No ETL job. If the files sit on S3 instead of local disk, the httpfs extension reads them straight from the bucket, and your Java code does not change beyond the path.
Step 4: Bulk inserts with the Appender
When you need to write rows fast, plain INSERTs are the slow path. The JDBC driver exposes DuckDB's Appender, which batches rows and flushes on close. Cast the connection to DuckDBConnection and use it inside try-with-resources:
try (var appender = duckDbConnection.createAppender(
DuckDBConnection.DEFAULT_SCHEMA, "events")) {
for (Event e : batch) {
appender.beginRow();
appender.append(e.id());
appender.append(e.amount());
appender.append(e.city());
appender.endRow();
}
} // close() flushes
This is the pattern to reach for when a nightly job materializes thousands of rows, or when you want your AI agent's telemetry landing somewhere queryable without touching the main transactional database.
Step 5: Keep the write path single-owner
The architecture that has worked for me: one scheduled job, or one service instance, owns the DuckDB file in read-write mode and does the ingesting. Everything else, dashboards, APIs, ad-hoc analysis, either reads through that service or opens the file read-only. This sidesteps the write-concurrency model entirely and matches how DuckDB is designed to be used today.
What to revisit when v2.0 lands this fall
The release is a preview and details may shift, but three items belong on your radar now:
-
Quack server mode.
CALL quack_serve(token = '...')on one side,ATTACH 'quack:server.example.com'plusCONNECTon the other, with results streaming back. When the JDBC driver catches up, the single-writer topology above becomes optional: the owning process can serve queries over the network instead of sharing files. -
The storage format bump. v2.0 makes a new format the default, and it is a breaking change. Old DuckDB binaries will not read v2.0-format files. If multiple tools touch your
.duckdbfiles, upgrade them in lockstep, or keep an export path. -
VARIANT for messy JSON. If you are currently landing semi-structured payloads as JSON strings and parsing at query time, VARIANT's shredded storage plus the new
variant_*functions are worth a spike. The docs position it as a natural fit for real-time log ingestion where the schema evolves, which is exactly the AI-agent-telemetry shape I deal with.
Decision guide: when DuckDB, when Postgres
Do not take the "Postgres killer" bait from the HN comment section. These tools solve different problems. Here is how I decide:
- Use PostgreSQL when you have many concurrent writers, strict transactional guarantees across services, mature tooling requirements (replication, extensions like PostGIS), and row-oriented access patterns.
- Use DuckDB when the workload is analytical, the data lands as files (Parquet, CSV, JSON), the team is small, and standing up a warehouse is disproportionate to the problem. Also when you want a query engine embedded in a batch job with zero deployment footprint.
- Use both when Postgres runs the product and DuckDB answers the reporting questions. The v2.0 CONNECT pushdown, which ships SQL to a PostgreSQL server instead of pulling tables, makes this pairing explicitly a designed-for combination rather than a hack.
The takeaway I would hand my past self
The biggest shift in v2.0 is not any single feature. It is that DuckDB is outgrowing the "cool embedded toy" niche and asserting itself as infrastructure. A columnar, MVCC, transactional engine that embeds in your Spring Boot jar today and speaks client/server tomorrow collapses a category of architecture that used to require a dedicated platform team.
My save-worthy checklist for adopting it:
- Start with read-only analytics over Parquet files, the lowest-risk, highest-value pattern.
- Keep one process as the single writer of any database file.
- Pin the JDBC driver version and treat storage-format upgrades as coordinated events.
- Watch the quack extension and JDBC driver releases this fall before designing multi-service access.
- Do not migrate OLTP workloads to it. That is not what it is for, no matter what the comment section says.
I write about Java, Spring Boot, and AI every week. Subscribe, it is free.
Have you used DuckDB in a JVM service yet, or were you waiting for a server mode like I was? What tipped your decision, and what broke? I read every comment.
Sources: the DuckDB v2.0 preview announcement by Mark Raasveldt and Hannes Mühleisen, the Hacker News discussion, and the official DuckDB Java JDBC documentation.
Top comments (0)