Fulcro RAD (Rapid Application Development) is built on a layered architecture where attributes serve as the universal schema, carrying extensible facts that drive every other subsystem.
Source diagram is in architecture.d2 (D2 language).
Regenerate with: d2 --layout=dagre architecture.d2 architecture.svg
|
Attributes are open maps that carry core facts (qualified key, type, target, cardinality, identities, schema) plus contextual metadata under namespaced keys.
Any subsystem can attach its own keys:
| Context | Prefix | Examples |
|---|---|---|
Form |
|
|
Report |
|
|
Database |
|
|
Resolver |
|
|
Custom | any namespace | DB adapters, auth, renderers attach their own metadata |
This open-map design means two facts about the same attribute at the same time can coexist without conflict — the form context and the report context are simply different namespaced keys on the same attribute.
Forms and Reports are application behavior components that read their configuration from attribute metadata. Each has:
A render plugin provides platform-specific rendering (Semantic UI, MUI, etc.). The abstract component says what to render; the plugin says how.
A UISM (UI State Machine) controls the lifecycle.
Forms: create → load → edit → submit → success/fail.
Reports: load → filter → sort → paginate.
Override via fo/machine or ro/machine to customize behavior.
Each layer does its own job. The composition of middleware is the feature.
RAD provides you with a minimum diff:
{[:person/id 1]
{:person/name {:before "Alice"
:after "Bob"}}}
Only actually-changed attributes are transmitted. This matters for concurrency:
Two users editing different fields on the same entity will not collide.
The :before value is a hint, not a guarantee — the server must verify
it against the real DB state before applying the change.
Maps directly to the Datomic fact model: retract old value, assert new value.
|
The server’s "truths" at various points in time could be cached on different clients. You’re trying to reason about operations from those distributed clients, to make sure your final state is always valid. The server should verify the value being retracted actually exists at that time. |
The save path is a composable middleware pipeline where each layer wraps the next handler:
gc-orphans → BT save-middleware → datomic
Separation of concerns applies to middleware too.
Each middleware can:
Rewrite values — type coercion, encryption, format conversion
Validate — business rules, authorization checks
GC orphans — clean up component entities on retraction (via ao/component? true)
Detect conflicts — verify :before values against current DB state
Don’t reimplement lifecycle management that the framework already provides.
Resolvers are the parameterizable read mechanism:
Attributes with ao/pc-resolve / ao/pc-output automatically produce
Pathom resolvers. Schema + identities → CRUD.
Datomic, SQL, etc. provide resolver generators that read attribute schema metadata to build queries.
Resolvers compose into a Pathom graph. The client sends EQL; the graph satisfies it from available resolvers. No hand-written endpoints needed.
Attributes ──fo/──→ Forms ──dirty-fields──→ Minimum Delta
│ │
├──ro/──→ Reports ▼
│ ▲ Save Middleware
└──schema──→ Resolvers ───EQL read──→ Forms / Reports
▲ │
└────── mutations ─────────┘
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 |