bb scaffold generate --module-name product --entity Product \
--field name:string:required --field sku:string:required --field price:decimal
After bb scaffold generate and bb scaffold integrate, Boundary gives you a module that already fits the framework. The point is to create the right shape of files, not just a set of files.
Run it from your project root:
bb scaffold generate --module-name product --entity Product \
--field name:string:required --field sku:string:required --field price:decimal
For that product module the scaffolder creates the familiar FC/IS layout under src/boundary/product/:
src/boundary/product/
├── core/
│ ├── product.clj ← Pure business rules
│ └── ui.clj ← UI helpers and presentation logic
├── shell/
│ ├── persistence.clj ← Database translation and queries
│ ├── service.clj ← Orchestration and validation
│ ├── http.clj ← Request and response handlers
│ └── web_handlers.clj
├── ports.clj ← Protocol definitions
└── schema.clj ← Malli validation schemas
The scaffolder emits modules under the boundary.<module>.* namespace (e.g. boundary.product.core.product) into src/boundary/. Wiring the module into deps.edn/tests.edn currently assumes the monorepo libs/ layout — see the scaffolder reference for the current integration flow.
|
If you have seen one Boundary module, you already know the basic map of the next one.
schema.clj defines the shape of the entity and the input data using Malli:
(def Product
[:map
[:id :uuid]
[:name [:string {:min 1 :max 200}]]
[:sku [:string {:min 1 :max 50}]]
[:price [:decimal {:min 0}]]
[:created-at inst?]
[:updated-at inst?]])
(def ProductInput
[:map
[:name [:string {:min 1 :max 200}]]
[:sku [:string {:min 1 :max 50}]]
[:price [:decimal {:min 0}]]])
All keys are kebab-case internally. Conversion to snake_case for the database or camelCase for the API happens only at the boundary. See Conventions.
|
core/product.clj contains only pure functions:
(ns boundary.product.core.product
(:require [boundary.product.schema :as schema]
[boundary.core.utils.validation :as v]))
(defn prepare-product
"Validates input and returns a product entity ready for persistence."
[input now]
(let [entity (merge {:id (random-uuid)
:created-at now
:updated-at now}
input)]
(v/validate-with-transform schema/Product entity)))
No I/O. No side effects. Easy to test without mocks.
ports.clj defines the interface between layers:
(ns boundary.product.ports)
(defprotocol IProductRepository
(create-product! [this product])
(find-product-by-id [this id])
(list-products [this opts])
(update-product! [this id changes])
(delete-product! [this id]))
(defprotocol IProductService
(create-product [this input])
(get-product [this id])
(list-products [this opts]))
shell/service.clj orchestrates validation, core logic, and persistence:
(defn create-product [this input]
(let [[valid? errors data] (validate ProductInput input)]
(if valid?
;; The clock belongs in the shell so the core stays deterministic and testable.
(let [now (java.time.Instant/now)
product (product-core/prepare-product data now)]
(persistence/create-product! (:repo this) product))
(throw (ex-info "Validation failed"
{:type :validation-error :errors errors})))))
now is supplied by the shell because time is an external dependency, not part of the business rule itself.
That split is deliberate:
core/ decides what should happen
shell/ decides how it happens in the real world
A project generated with boundary new runs its tests through Kaocha via the :test alias:
clojure -M:test
If you only changed pure logic, run the unit suite:
clojure -M:test :unit
clojure -M:test:db/h2 :product is the monorepo form — inside this framework repository, where each library has its own Kaocha suite id and the :db/h2 alias selects the in-memory database. Generated projects use the plain :test alias shown above.
|
Validation - using Malli schemas and the validation framework
Authentication - protecting your endpoints
Functional Core / Imperative Shell - deeper dive into the pattern
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 |