Liking cljdoc? Tell your friends :D

Your First Module

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.

What you get

For a product module with name, sku, and price fields, the scaffolder creates the familiar FC/IS layout:

libs/product/
├── src/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
├── test/product/
│   ├── core/product_test.clj          ← Unit tests
│   ├── shell/service_test.clj         ← Integration tests
│   └── shell/product_repository_test.clj     ← Contract tests
└── migrations/005_create_products.sql

If you have seen one Boundary module, you already know the basic map of the next one.

The schema

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.

The core

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.

The ports

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]))

The shell

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

Run the module’s tests

clojure -M:test:db/h2 :product

If you only changed pure logic, you can also run the unit-focused suite:

clojure -M:test:db/h2 --focus-meta :unit

Next steps

  • 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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close