Collaborators
What is a Collaborator?
A collaborator is an external dependency injected into a command handler. Collaborators represent interactions with the outside world — sending emails, processing payments, calling third-party APIs — and are designed to be swappable between implementations.
Collaborators in Bounda
When a command needs external dependencies, it uses a folder structure instead of a single file. The folder contains the command definition (index.ts) and one or more collaborator implementation files.
domain/
order/
commands/
send-confirmation/
index.ts
notification-sender.console.ts
notification-sender.sendgrid.ts
File Naming Convention
Collaborator files follow the pattern:
<collaborator-name>.<implementation-name>.ts
- Collaborator name: describes the role (e.g.,
notification-sender,payment-gateway) - Implementation name: identifies the specific implementation (e.g.,
console,sendgrid,stripe)
Defining a Collaborator Implementation
Each collaborator file exports a named function matching the camelCase form of the collaborator name:
import type { Command } from "./+types/send-confirmation";
export const notificationSender = async ({ state }: Command.CollaboratorArgs) => {
console.log(`Order confirmation sent for order ${state.id}`);
};
A production implementation might look like:
import type { Command } from "./+types/send-confirmation";
export const notificationSender = async ({ state, config }: Command.CollaboratorArgs) => {
await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: { Authorization: `Bearer ${config.apiKey}` },
body: JSON.stringify({
to: state.email,
subject: "Order Confirmation",
text: `Your order ${state.id} has been confirmed.`,
}),
});
};
Using Collaborators in Command Handlers
The command handler receives collaborators as named arguments alongside the standard command, events, and stateResolver:
import type { Command } from "./+types/send-confirmation";
export async function handler({ stateResolver, notificationSender, events }: Command.HandlerArgs) {
const state = await stateResolver();
await notificationSender({ state });
return events.confirmationSent({ state });
}
The collaborator function is fully typed based on the implementation’s CollaboratorArgs interface, generated by Bounda’s code generation.
Configuration
The bounda.config.ts file selects which implementation to use for each collaborator. The use property maps to the implementation name in the file name:
commands: {
sendConfirmation: {
notificationSender: {
use: "console",
},
},
}
This configuration selects notification-sender.console.ts as the active implementation. Switching to SendGrid in production is a one-line config change:
commands: {
sendConfirmation: {
notificationSender: {
use: "sendgrid",
apiKey: process.env.SENDGRID_API_KEY,
},
},
}
Passing Options to Collaborators
Any extra properties in the collaborator configuration (beyond use) are passed to the implementation via the config argument in CollaboratorArgs:
export const notificationSender = async ({ state, config }: Command.CollaboratorArgs) => {
const apiKey = config.apiKey;
// use apiKey to authenticate with the service
};
Guidelines
- Use collaborators for any interaction that crosses the system boundary: network calls, file system access, third-party services.
- Always provide at least one simple implementation (e.g.,
consoleorin-memory) for local development and testing. - Keep the collaborator interface narrow. The implementation should do one thing — the command handler orchestrates the broader flow.
- Name collaborators by their role (
notification-sender), not by their technology (sendgrid-client). This keeps the command handler decoupled from specific providers.
Related Concepts
- Commands — collaborators are injected into command handlers
- Code Generation — generates typed interfaces for collaborator arguments