Backend architecture
August 14, 2026 · View on GitHub
The backend is organized by feature. A feature owns its domain workflows,
effects, ports, adapters, and SQL sources. Shared runtime and composition code
lives under src/glot_backend/system.
Feature layout
A feature may use the following structure:
src/glot_backend/<feature>/
├── domain/ Business workflows and decisions
├── effect/
│ ├── algebra.gleam Effect types and trace names
│ ├── effect.gleam Program constructors
│ └── interpreter.gleam Effect dispatch to ports
├── ports.gleam Optional bundle of feature dependencies
├── ports/ Individual dependency interfaces
├── adapter/ Infrastructure implementations of ports
└── sql/ Feature-owned Parrot SQL sources
Small features do not need every directory or a ports bundle. The structure should reflect actual boundaries rather than create empty layers.
The intended dependency flow is:
domain workflows → effect API → effect algebra
↓
effect interpreter → ports ← adapters
↑
application composition
- Domain workflows call effect APIs and remain independent of concrete infrastructure.
- Effect algebras describe required operations. Effect interpreters execute those operations through ports.
- Ports define the interfaces a feature needs. Adapters implement them for PostgreSQL, external services, caches, or tests.
- Composition code constructs adapters and supplies them to interpreters.
Features with subfeatures
When a feature contains several related effect algebras, the feature root owns their integration with the global program:
effect/algebra.gleamcombines subfeature effects into one feature effect.effect/effect.gleamis the only feature module that constructs the globalprogram_typesvariant.effect/interpreter.gleamdispatches the combined effect to subinterpreters.ports.gleambundles the dependencies accepted by the root interpreter.- The root effect algebra combines subfeature trace names and delegates their string conversion to the owning subfeature algebra.
Auth, job, and logging are the reference implementations. Adding an operation inside one of these features should require changes only in the owning subfeature and its feature-root boundary. It should not add another global DB effect, global trace variant, or individual dependency to the system interpreter.
Composition boundaries
Global composition is intentionally centralized:
system/effect/program_types.gleamdefines the closed set of application and database feature effects.system/effect/db_effect.gleammaps database feature effects.system/effect/db_interpreter.gleamdelegates database effects to feature interpreters.system/effect/effect_trace.gleamdelegates trace naming to feature algebras.system/effect/database_ports.gleamcontains database-backed feature dependencies.system/effect/service_ports.gleamandsystem/effect/system_ports.gleamcompose the complete runtime dependencies.
Adding an entirely new feature requires updating these composition roots. Adding a subfeature or operation to an existing bundled feature should not.
Effect interpreter measurements
Effect interpreters should use system/effect/measured_interpreter.gleam for
the standard operation, continuation, and trace-measurement lifecycle. Choose
the helper according to the operation's semantics:
runmeasures an operation and passes its returned value unchanged to the effect continuation. A returnedResultremains a value for the effect program to handle.run_or_failmeasures an operation returningResult, passes a successful value to the effect continuation, and maps a failure directly to the application error surface without invoking the continuation.run_with_kindis for operations whoseEffectKindis known only after execution, such as a lookup classified by its cache outcome.run_with_stateis for operations that updateprogram_state.Statebefore continuing, such as collecting structured log fields.
At call sites, keep the operation and effect continuation as the first two positional arguments. Pass error mapping, trace metadata, interpreter state, and the interpreter continuation with labels so their roles remain explicit.
Keep feature-specific decisions and port composition in the feature interpreter or a small local helper. Pass that operation to the measurement helper rather than moving business behavior into the shared module.
Transaction interpretation is intentionally different. It owns its timing so the transaction trace can include nested effect measurements and whether the transaction rolled back. Do not route that aggregate lifecycle through the ordinary measurement helpers.
Effect program construction
Effect constructor modules should use the shared program helpers rather than
constructing Impure, DbEffect, or TxImpure wrappers directly:
program.performlifts an application effect intoProgram.program.perform_dblifts a database effect intoProgram.transaction_program.performlifts a database effect intoTransactionProgram.
Use program.from_mapped_result or
transaction_program.from_mapped_result when an effect continuation needs to
convert a port-specific error into the application error surface. Keep a
feature-local effect builder when both normal and transactional programs use
the same algebra operation with different continuations.
Database boundary
SQL sources live with their feature but are generated together into
src/glot_backend/sql.gleam by ../run_parrot.sh. Generated query and row
types stay in the database layer. Convert SQL rows into domain types at the
program/handler or adapter boundary before returning them to domain code.
Domain modules must never import glot_backend/sql.
Tests
Tests use the same ports as production code. Test adapters live under
test/support, and default test ports should fail on unexpected calls. A test
enables or replaces only the feature dependencies it exercises.
After backend changes, run:
cd glot_backend
gleam test