This guide provides comprehensive instructions for configuring, managing, and troubleshooting databases in the Boundary Framework. Boundary supports multiple database engines through a unified abstraction layer, allowing you to switch between them with minimal configuration changes.
Boundary implements a Functional Core / Imperative Shell architecture. The database layer is part of the "Imperative Shell," where side effects like I/O and persistence are handled. The framework provides a database-agnostic interface that allows developers to write business logic without worrying about the specific SQL dialect of the underlying database.
Boundary provides first-class support for the following databases:
| Database | Primary Use Case | Driver | Status |
|---|---|---|---|
| SQLite | Local development, small apps, zero-config | org.xerial/sqlite-jdbc | Production Ready |
| PostgreSQL | Production, enterprise apps, JSON support | org.postgresql/postgresql | Production Ready |
| H2 | In-memory testing, CI/CD, fast iteration | com.h2database/h2 | Testing Only |
:user-id in Clojure code; the framework's persistence layer automatically handles conversion to user_id for SQL queries and back to :user-id for results.Choosing the right database depends on your project's phase and scale.
| Feature | SQLite | PostgreSQL | H2 |
|---|---|---|---|
| Type | Embedded (File) | Client-Server | Embedded/Memory |
| Setup | Zero config, file-based | Requires server/Docker | Zero config, memory-based |
| Concurrency | Limited (WAL mode helps) | High (Optimistic/Pessimistic) | Medium |
| Transactions | Strong (ACID) | Strong (ACID) | Strong (ACID) |
| JSON Support | Via extension | Native (JSONB) | Limited |
| Backup | File copy | pg_dump, WAL archiving | Memory-only (lost on restart) |
| Best for | Prototyping, Mobile apps | Production, High-traffic | Unit/Contract Tests, CI |
SQLite is the default for local development. It stores data in a single file and requires no server installation, making it perfect for "clone and run" developer experiences.
deps.edn)Ensure the driver is included in your dependencies:
{:deps {org.xerial/sqlite-jdbc {:mvn/version "3.51.0.0"}}}
config.edn)SQLite configuration is simple. It mainly requires the path to the database file.
:boundary/db-context
{:adapter :sqlite
:database-path "dev-database.db"
:pool {:minimum-idle 1
:maximum-pool-size 5
:connection-timeout-ms 10000}}
foreign_keys = ON, journal_mode = WAL, and synchronous = NORMAL.PostgreSQL is the recommended database for production. It offers enterprise-grade features, advanced JSONB support for semi-structured data, and excellent performance for high-concurrency workloads.
deps.edn){:deps {org.postgresql/postgresql {:mvn/version "42.7.8"}}}
config.edn)Use environment variables via Aero's #env tag for sensitive credentials and environment-specific settings:
:boundary/db-context
{:adapter :postgresql
:host #or [#env POSTGRES_HOST "localhost"]
:port #long #or [#env POSTGRES_PORT 5432]
:name #or [#env POSTGRES_DB "boundary_dev"]
:username #or [#env POSTGRES_USER "postgres"]
:password #or [#env POSTGRES_PASSWORD "postgres"]
:pool {:minimum-idle 5
:maximum-pool-size 20
:connection-timeout-ms 30000
:idle-timeout-ms 600000
:max-lifetime-ms 1800000}}
A standard Docker setup for local PostgreSQL development:
docker run --name boundary-db \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=boundary_dev \
-p 5432:5432 \
-d postgres:15-alpine
To persist data between Docker restarts, add a volume:
docker run --name boundary-db \
-v boundary-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
-d postgres:15-alpine
H2 is an extremely fast Java-based database. In Boundary, it is primarily used in its in-memory mode for testing.
deps.edn){:deps {com.h2database/h2 {:mvn/version "2.4.240"}}}
config.edn)For testing, use the :memory path.
:boundary/db-context
{:adapter :h2
:database-path "mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL"
:pool {:minimum-idle 1
:maximum-pool-size 10}}
DB_CLOSE_DELAY=-1: This is critical for in-memory databases. It prevents H2 from closing the database when the last connection is dropped, ensuring data persists during the life of the JVM.MODE=PostgreSQL: H2 can emulate other databases. We recommend using PostgreSQL mode to keep your SQL syntax compatible with your production target.Boundary uses HikariCP, widely regarded as the fastest and most reliable connection pool for the JVM.
| Parameter | Default | Description |
|---|---|---|
:minimum-idle | 2 | The minimum number of idle connections HikariCP tries to maintain in the pool. |
:maximum-pool-size | 10 | The maximum number of connections the pool is allowed to reach, including both idle and in-use connections. |
:connection-timeout-ms | 30000 | The maximum number of milliseconds that a client will wait for a connection from the pool. |
:idle-timeout-ms | 600000 | The maximum amount of time that a connection is allowed to sit idle in the pool. |
:max-lifetime-ms | 1800000 | The maximum lifetime of a connection in the pool. Connections should be retired every 30-60 minutes to avoid stale connections. |
:validation-timeout-ms | 5000 | The maximum amount of time that the pool will wait for a connection to be validated as alive. |
maximum-pool-size = 10 per instance is more than enough.(2 * core_count) + effective_spindle_count.instances * maximum-pool-size does not exceed the database server's max_connections (usually 100 for PostgreSQL).Boundary uses Migratus for version-controlled schema changes. Migrations are plain SQL files located in libs/platform/resources/migrations/.
Each migration consists of an up file (to apply changes) and a down file (to revert changes).
Example: 20260126100000-create-users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Example: 20260126100000-create-users.down.sql
DROP TABLE users;
The CLI tool handles migration execution. Ensure you run these from the root directory.
| Command | Action |
|---|---|
clojure -M:migrate up | Applies all pending migrations. |
clojure -M:migrate rollback | Reverts the single most recent migration. |
clojure -M:migrate reset | Reverts all migrations and then re-applies them (use with caution!). |
clojure -M:migrate pending | Lists migrations that have not been applied yet. |
If you need to change where migrations are stored, update your :boundary/db-context config:
:boundary/db-context
{:adapter :postgresql
:migration-dir "custom/migrations/"
...}
The framework uses HoneySQL for query building, which abstracts away most dialect differences.
UUID in PostgreSQL, as TEXT in SQLite, and UUID in H2.BOOLEAN in PostgreSQL/H2, INTEGER (0 or 1) in SQLite.TIMESTAMP WITH TIME ZONE in PostgreSQL, TEXT (ISO8601) in SQLite.Boundary provides a unified upsert! function that works across all supported databases, translating to ON CONFLICT for PostgreSQL, INSERT OR REPLACE for SQLite, and MERGE for H2.
Every database operation is automatically wrapped in observability interceptors.
Slow queries and errors are logged with full context:
# View slow queries in dev
tail -f logs/boundary.log | grep "SLOW QUERY"
The following metrics are exported (if a provider like Datadog is configured):
db.query.duration: Histogram of query execution time.db.pool.active_connections: Number of connections currently in use.db.pool.idle_connections: Number of connections available in the pool.db.errors: Count of database exceptions by type and query.POSTGRES_USER and POSTGRES_PASSWORD.psql or DBeaver using the same credentials.:maximum-pool-size.SHOW max_connections; in PostgreSQL.To maintain a healthy database layer in Boundary, follow these best practices:
["SELECT * FROM users WHERE id = ?" id] to prevent SQL injection.config.edn. Use Aero's #env tag to load them from the environment.WHERE clause or JOIN condition is indexed, especially as your data grows.Boundary's database system is designed to grow with your application. By providing a consistent API and robust infrastructure for SQLite, PostgreSQL, and H2, we ensure that you can focus on building features while we handle the complexities of data persistence and scalability.
For more information, see the Architecture Guide or join our community discussions.
Last Updated: 2026-01-26
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |