Postboy Help

Best Practices

These recommendations help keep executors readable, testable, and well‑scoped.

1. One executor = one operation

If an executor starts to handle multiple unrelated tasks, split it.
A single responsibility makes the contract obvious and the handler easy to test.

// ✅ clear purpose export class ValidateDiscountExecutor extends PostboyExecutor<boolean> { static readonly ID = 'discount.validate'; constructor(public readonly code: string) { super(); } }

2. Small, explicit contract

Pass only the data needed to perform the operation.
Avoid adding flags or state that belong elsewhere.

export class RefreshCacheExecutor extends PostboyExecutor<boolean> { static readonly ID = 'cache.refresh'; constructor(public readonly scope: string) { super(); } }

3. Choose the right registration model

Use direct function for thin wrappers.

postboy.exec(new ConnectExecutor(FormatDateExecutor, (executor) => { return Intl.DateTimeFormat('en-US').format(executor.date); }));

Use a handler class when the logic grows beyond a few lines.

class RefreshCacheHandler extends PostboyExecutionHandler<boolean, RefreshCacheExecutor> { handle(executor: RefreshCacheExecutor): boolean { // complex multi‑step logic return cacheService.refresh(executor.scope); } } postboy.exec(new ConnectHandler(RefreshCacheExecutor, new RefreshCacheHandler()));

4. Keep the result type meaningful

Prefer explicit result shapes over ambiguous values.

// ✅ caller knows what happened type RefreshResult = { success: true } | { success: false; reason: string }; export class RefreshCacheExecutor extends PostboyExecutor<RefreshResult> { static readonly ID = 'cache.refresh'; constructor(public readonly scope: string) { super(); } }

5. Do not use executors for domain events

If the operation is a notification that something happened, use a PostboyGenericMessage, not an executor.

// ✅ event — no result expected export class OrderShippedEvent extends PostboyGenericMessage { static readonly ID = 'order.shipped'; constructor(public readonly orderId: string) { super(); } }

6. Keep errors in application code

Postboy does not own your errors. If the handler can fail, either throw (fast‑fail) or return an error‑shaped result.

try { const valid = postboy.exec(new ValidateDiscountExecutor('')); } catch (e) { console.error(e.message); }

7. Separate domain executors from infrastructure executors

Infrastructure executors (ConnectMessage, ConnectExecutor, ConnectHandler) wire the bus.
Domain executors carry business operations. Keep this distinction visible in your code.

messages/ executors/ product-to-view-model.executor.ts ← domain cache-refresh.executor.ts ← domain bus/ registration.ts ← infrastructure executors called here

8. Test handler logic in isolation

Executor handlers are pure functions — test them without the bus.

test('formats a date', () => { const handler = new FormatDateHandler(); const result = handler.handle(new FormatDateExecutor(new Date(2026, 3, 23))); expect(result).toBe('4/23/2026'); });

9. Summary

  • Keep executors small and focused.

  • Use the simplest registration model that fits the complexity.

  • Make results explicit.

  • Test handlers directly.

  • Let Postboy coordinate; let your application own the logic.

03 мая 2026