| name | blong-orchestrator |
| description | Implement business logic coordination in Blong, decoupled from integration protocols. Orchestrators coordinate between adapters, define API namespaces, and become Kubernetes services. Use this skill whenever implementing business workflows, coordinating multiple adapter calls, adding business rules, defining new namespaces, or translating sequence diagrams to code — even without the word 'orchestrator'. |
Implementing an Orchestrator
Overview
Orchestrators implement business logic decoupled from protocols. They coordinate between adapters, define namespaces (which become Kubernetes services in microservice mode), and implement patterns like sagas for distributed transactions.
File Structure
orchestrator/
├── dispatch.ts # Orchestrator definition
├── entity1/ # Handler group: realmname.entity1
│ ├── ~.schema.ts # Auto-generated validation
│ ├── helper.ts # Library function
│ ├── realmEntity1Add.ts # Business handler
│ └── realmEntity1Edit.ts
└── entity2/ # Handler group: realmname.entity2
├── ~.schema.ts
├── realmEntity2Find.ts
└── validate.ts # Library function
Built-in Orchestrators
1. Dispatch Orchestrator
The most common orchestrator type. Enables calling attached handlers using configured namespaces, with optional fallback to another destination.
Use Cases:
- Standard business logic implementation
- Coordinating between different entities
- Fallback to database adapter when no handler exists
Implementation:
import {orchestrator} from '@feasibleone/blong';
export default orchestrator(blong => ({
extends: 'orchestrator.dispatch',
activation: {
default: {
namespace: ['entity1', 'entity2'],
imports: ['realmname.entity1', 'realmname.entity2'],
validations: ['realmname.entity1.validation'],
destination: 'sql',
logLevel: 'info'
}
}
}));
2. Schedule Orchestrator
Invokes functionality based on predefined schedules using cron patterns.
Use Cases:
- Periodic batch processing
- Scheduled reports
- Cleanup tasks
- Health checks
Implementation:
import {orchestrator} from '@feasibleone/blong';
export default orchestrator(blong => ({
extends: 'orchestrator.schedule',
activation: {
default: {
namespace: ['batch'],
imports: ['realmname.batch'],
schedule: {
batchProcessDaily: '0 2 * * *',
batchCleanup: '0 0 * * 0',
batchHealthCheck: '*/5 * * * *'
}
}
}
}
Handler Implementation in Orchestrator
Business Logic Handler
import {IMeta, handler} from '@feasibleone/blong';
type Handler = ({
username: string;
email: string;
role: string;
}) => Promise<{
userId: number;
username: string;
}>;
export default handler(({
lib: {
validateEmail // Library function
},
errors,
handler: {
sqlUserFind, // Adapter handler
sqlUserAdd // Adapter handler
}
}) =>
async function userUserAdd(
params: Parameters<Handler>[0],
$meta: IMeta
): ReturnType<Handler> {
if (!validateEmail(params.email)) {
throw errors.invalidEmail();
}
const existing = await sqlUserFind({username: params.username}, $meta);
if (existing.length > 0) {
throw errors.userExists();
}
const result = await sqlUserAdd(params, $meta);
return {
userId: result.userId,
username: result.username
};
}
);
Library Function
import {library} from '@feasibleone/blong';
export default library(() =>
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
);
Configuration Properties
Common Properties
orchestratorname: {
namespace: ['entity1', 'entity2'],
imports: ['realmname.entity1', 'realmname.entity2'],
imports: [/^realmname\./],
validations: ['realmname.entity1.validation'],
validations: [/^realmname\.\w+\.validation$/],
destination: 'sql',
logLevel: 'info'
}
Schedule-Specific Properties
schedule: {
namespace: ['batch'],
imports: ['realmname.batch'],
schedule: {
handlerName1: '0 0 * * *',
handlerName2: '*/15 * * * *'
}
}
Orchestration Patterns
Simple Orchestration
Single adapter call with transformation:
export default handler(({handler: {sqlUserFind}}) =>
async function userUserGet(params, $meta) {
const users = await sqlUserFind(params, $meta);
return users.map(user => ({
id: user.userId,
name: user.username
}));
}
);
Multi-Adapter Orchestration
Coordinate between multiple adapters:
export default handler(({
handler: {
sqlUserFind,
sqlPermissionFind,
httpNotificationSend
}
}) =>
async function userUserGrantPermission(params, $meta) {
const user = await sqlUserFind({userId: params.userId}, $meta);
await sqlPermissionFind({
userId: params.userId,
permission: params.permission
}, $meta);
await httpNotificationSend({
email: user.email,
subject: 'Permission Granted',
body: `You have been granted ${params.permission}`
}, $meta);
return {success: true};
}
);
Cross-Realm Orchestration
Call orchestrators from other realms:
export default handler(({
handler: {
paymentTransferPrepare, // From payment realm
ledgerAccountDebit, // From ledger realm
sqlTransactionCreate // Local adapter
}
}) =>
async function transferMoneyBetweenAccounts(params, $meta) {
const tx = await sqlTransactionCreate(params, $meta);
const payment = await paymentTransferPrepare({
transactionId: tx.id,
amount: params.amount
}, $meta);
await ledgerAccountDebit({
accountId: params.fromAccount,
amount: params.amount,
reference: payment.id
}, $meta);
return {
transactionId: tx.id,
paymentId: payment.id
};
}
);
Saga Pattern (Distributed Transaction)
Implement compensation logic for failures:
export default handler(({
handler: {
paymentTransferPrepare,
ledgerAccountDebit,
paymentTransferCommit,
paymentTransferCancel,
ledgerAccountCredit
},
errors
}) =>
async function transferWithCompensation(params, $meta) {
let payment, debit;
try {
payment = await paymentTransferPrepare(params, $meta);
debit = await ledgerAccountDebit({
accountId: params.fromAccount,
amount: params.amount
}, $meta);
await paymentTransferCommit({paymentId: payment.id}, $meta);
return {success: true, paymentId: payment.id};
} catch (error) {
if (debit) {
await ledgerAccountCredit({
accountId: params.fromAccount,
amount: params.amount
}, $meta);
}
if (payment) {
await paymentTransferCancel({paymentId: payment.id}, $meta);
}
throw errors.transferFailed({cause: error});
}
}
);
Multiple Orchestrators Per Realm
When a realm has multiple concerns, create separate orchestrators — each co-locates its own config:
export default orchestrator(blong => ({
extends: 'orchestrator.dispatch',
activation: {default: {namespace: ['user'], imports: ['realmname.user']}}
}));
export default orchestrator(blong => ({
extends: 'orchestrator.dispatch',
activation: {default: {namespace: ['role'], imports: ['realmname.role']}}
}));
export default orchestrator(blong => ({
extends: 'orchestrator.dispatch',
activation: {default: {namespace: ['permission'], imports: ['realmname.permission']}}
}));
orchestrator/
├── userDispatch.ts
├── roleDispatch.ts
├── permissionDispatch.ts
├── user/
├── role/
└── permission/
Best Practices
- One Namespace Per Orchestrator: Keep orchestrators focused on a single business entity
- Protocol Independence: Don't reference protocol-specific details (HTTP, TCP, etc.)
- Business Logic Only: Keep technical concerns in adapters
- Call Adapters, Not Other Realms' Adapters: Call other orchestrators, not their adapters directly
- Error Handling: Use domain errors, implement compensation for distributed transactions
- Reusable Functions: Extract common logic to library functions
- Minimal Transformation: Do necessary business transformations, not format conversions
- Configuration Over Code: Use configuration for destinations and fallbacks
- Type Safety: Define Handler types for automatic validation
- Testing: Write comprehensive test handlers for orchestration logic
Deployment Considerations
- Microservices: Each orchestrator becomes a Kubernetes service
- Monolith: All orchestrators run in single process
- Service Discovery: Namespace becomes service name in Kubernetes
- Load Balancing: Framework handles load balancing between orchestrators
- Scaling: Orchestrators can scale independently
Examples from Codebase
- Simple dispatch:
core/test/demo/orchestrator/subjectDispatch.ts
- Multi-entity:
ml/agreement/orchestrator/agreementDispatch.ts
- Complex workflow:
ml/payment/orchestrator/