Carrier
Part Two · Core Language Model
Chapter 81 min read

Functions, Actions, and Transactions

Carrier separates pure helper logic, business operations, and transactional boundaries. This separation helps prevent service code from collapsing into route handlers.

When to Use fn

Use fn for pure helper logic that depends entirely on its arguments. A function should not be responsible for authentication, persistence, transactions, or runtime side effects. It is best used for calculations, formatting, classification, validation helpers, and deterministic domain rules.

When to Use action

Use action for business operations. Actions are the right home for workflows that are auth-aware, transactional, reusable, lock-heavy, or important enough to be called from more than one route. The booking-service has a small but representative example: locking a slot for review uses a transaction, reads with get_for_update, records a structured log, and is wrapped behind a single named action:

excerpt
action lock_slot_for_review(id: UUID) -> AppointmentSlot {
let actor = auth.current_user()
transaction {
let slot = AppointmentSlot.get_for_update(id: id, scope: "all")
logs.info("slot locked for review", {
action_name: "lock_slot_for_review"
slot_id: slot.id
actor_id: actor.id
actor_email: actor.email
})
}
return AppointmentSlot.get(id: id, scope: "all")
}

Notice the discipline. The action carries the business name (lock_slot_for_review). Identity is fetched once. The transaction defines a clear consistency boundary. Logs are structured — every key is one a governance tool can index. The route can stay thin because the work lives here.

Transaction Boundaries

Transactions define what must succeed or fail together. Enterprise systems often fail when transaction boundaries are accidental. A workflow updates one model, writes an audit event, calls another operation, and returns a response — but nobody clearly decided what happens if step three fails. Carrier encourages explicit transaction thinking. A transaction should protect a meaningful consistency boundary, not simply wrap everything by habit.

Reusable Business Logic Patterns

The most common Carrier service pattern is route → action → transaction. A stronger enterprise pattern is route → validate contract → call action → transaction → return typed response. This keeps responsibilities clear. Routes own HTTP shape. Types own contracts. Actions own business behavior. Transactions own consistency. Policies own access. That clarity is the difference between code that merely works and code that can be governed.

Contents