chainedFunction
chainedFunction solves a Clean Architecture coordination problem: inside a use case, you sometimes need to associate operations that update different entities. The chained function represents the aggregate that makes those operations part of the same business consistency boundary.
It lets the domain explicitly declare that several pure business actions are linked. Functions passed to chainedFunction do not inject dependencies and do not call ports: they only control entity lifecycle from the business point of view. The use case then orchestrates the aggregate, ports, and technical effects.
Interactive example
Why use it?
Use chainedFunction when a business aggregate is only consistent if several named operations happen together.
For example, publishing a comment can require:
- producing a valid comment entity;
- producing an updated article entity.
Persistence stays in the use case through the library port system. The chained function only models the aggregate contract: "creating the comment" and "incrementing the article comment count" are linked business actions.
INFO
In this context, repository remains a semantic alias of port through createRepository.
Guarantees
chainedFunction provides these guarantees at the type level:
- links are exposed one after another, in declaration order;
- the implementation cannot access a later link before calling the current one;
- the success path must end with
chainEnd(...); - a chained function can stop the flow by returning an
Either.Left; - the implementation can also return an
Either.Leftdirectly. - chained functions remain pure domain functions.
Syntax
Signature
function chainedFunction(
function1: [name: string, fn: Function, requirements?: C.RequirementsChainedFunction],
function2: [name: string, fn: Function, requirements?: C.RequirementsChainedFunction],
...functions: [name: string, fn: Function, requirements?: C.RequirementsChainedFunction][]
): ChainedFunctionImplementation
const aggregate = chainedFunction(...functions);
const result = aggregate(function *(firstLink, { breakIfLeft }) {
const [value, nextLink] = yield *firstLink(({ functionName }) => functionName(...args));
return chainEnd(value);
});Parameters
function1: first pure business function in the chain.function2: second pure business function in the chain.functions: additional pure business functions, executed in declaration order.requirements(optional tuple item on each function): typed required values that must be provided when calling the link generated for that function.firstLink: generated first link passed to the implementation callback.breakIfLeft: synchronous helper injected into the callback. It accepts a value that may contain anEither.Left, stops the chain when it is aLeft, otherwise returns the discriminated non-Leftvalue.
Return value
chainedFunction(...) returns a chained aggregate function. Calling it returns:
- the raw value passed to
chainEnd(value)on the success path; - the
Either.Leftreturned by a chained function; - the
Either.Leftreturned directly by the implementation.
Error flow
When a chained function returns an Either.Left, the generator yields it and chainedFunction stops the implementation before the next links run. Business errors should be represented as Either.Left; thrown exceptions and rejected promises are not caught.
breakIfLeft follows the same rule from inside the callback: use it to explicitly short-circuit from an intermediate synchronous value (value | Left) before calling the next link.
Requirements and lifecycle invariants
requirements are a typing tool for business lifecycle control. They force callers to provide specific typed values before a link can run.
Those values are not necessarily useful as runtime arguments for the next function. Their primary purpose is to prove, at compile time, that prerequisite information has been acquired earlier in the flow (validation done, authorization context loaded, prior step completed, etc.).
Requirements example
See also
useCase- Calls application logic with dependencies.port- Declares a port contract.repository- Semantic alias ofcreatePort.Either- Represents explicit success and error values.
