Key Concepts
If the quickstart showed you how to run Boundary, this page explains why the codebase is shaped the way it is.
Functional Core / Imperative Shell
The most important concept in Boundary is the split between pure business logic and side effects:
┌─────────────────────────────────────────────┐
│ IMPERATIVE SHELL (shell/*) │
│ I/O, validation, logging, side effects │
└─────────────────────────────────────────────┘
↓ calls
┌─────────────────────────────────────────────┐
│ PORTS (ports.clj) │
│ Protocol definitions (interfaces) │
└─────────────────────────────────────────────┘
↑ implements
┌─────────────────────────────────────────────┐
│ FUNCTIONAL CORE (core/*) │
│ Pure functions, business logic only │
└─────────────────────────────────────────────┘
Dependency rules:
-
Shell → Core: allowed
-
Core → Ports: allowed
-
Shell → Adapters: allowed
-
Core → Shell: never
See Functional Core / Imperative Shell for the full explanation.
Ports and adapters
Ports are Clojure protocols that define what a component can do, without specifying how:
;; ports.clj — the interface
(defprotocol IUserRepository
(find-user-by-email [this email])
(create-user! [this user]))
;; shell/adapters/postgres.clj — one implementation
(defrecord PostgresUserRepo [db-ctx]
IUserRepository
(find-user-by-email [this email] ...))
;; shell/adapters/in-memory.clj — test implementation
(defrecord InMemoryUserRepo [store]
IUserRepository
(find-user-by-email [this email] ...))
Swapping the implementation requires zero changes to business logic.
Case conventions
This is a frequent source of bugs. Boundary uses three different naming conventions at different boundaries:
| Boundary | Convention | Example |
|---|---|---|
All Clojure code |
|
|
Database (at boundary only) |
|
|
API/JSON (at boundary only) |
|
|
Always use boundary.core.utils.case-conversion for conversions. Never convert manually.
(require '[boundary.core.utils.case-conversion :as cc])
;; DB record → Clojure entity
(cc/snake-case->kebab-case-map db-record)
;; Clojure entity → DB record
(cc/kebab-case->snake-case-map entity)
;; Clojure entity → API response
(cc/kebab-case->camel-case-map entity)
See Conventions for the full guide.
Integrant lifecycle
Boundary uses Integrant for dependency injection and system lifecycle management.
All components are defined as Integrant keys in config.edn.
(go) ; Start all components
(reset) ; Reload changed namespaces and restart
(halt) ; Stop all components
|
Note
|
After changing a defrecord, use (halt) + (go) instead of reset.
reset does not recreate existing records.
|
Malli schemas
Boundary uses Malli for validation and schema definitions.
Schemas live in schema.clj at the root of each module:
(def UserInput
[:map
[:email [:string {:min 1 :max 255}]]
[:password [:string {:min 8 :max 128}]]
[:name {:optional true} :string]])
Testing pyramid
| Type | Metadata | What it tests |
|---|---|---|
Unit |
|
Pure core functions - no mocks, no DB |
Integration |
|
Shell services with mocked adapters |
Contract |
|
Adapters against real H2 in-memory DB |
clojure -M:test:db/h2 --focus-meta :unit
clojure -M:test:db/h2 --focus-meta :integration
clojure -M:test:db/h2 --focus-meta :contract
See Testing Strategy for the full guide.