What Design Patterns Does Corsair Use? A Deep Dive into 8 Architectural Patterns

Corsair uses eight core software design patterns—Factory, Builder, Registry, Proxy, Singleton, Strategy, Dependency Injection, and Facade—to create a modular, type-safe integration platform.

This article examines how the corsairdev/corsair repository implements these patterns in TypeScript. Each pattern serves a specific architectural purpose, from hiding construction complexity to enabling pluggable error handling across third-party service integrations.


Factory Pattern: Centralized Integration Creation

The Factory pattern in Corsair consolidates object creation behind a single entry point. In packages/corsair/core/index.ts, the createCorsair function handles the complex wiring of databases, encryption keys, permissions, and hub configuration.

// src/packages/corsair/core/index.ts
export function createCorsair<const Plugins extends readonly CorsairPlugin[]>(
  config: CorsairIntegration<Plugins>,
): CorsairSingleTenantClient<Plugins> | CorsairTenantWrapper<Plugins> {
  // …implementation omitted for brevity
}

Usage example:

import { createCorsair } from 'corsair/core';
import { slack } from 'packages/slack';

const corsair = createCorsair({
  plugins: [slack],
  database: { /* kysely DB config */ },
  kek: 'super-secret-kek',
  multiTenancy: false,
});

This factory approach lets developers instantiate fully-configured clients without understanding internal dependency graphs.


Builder Pattern: Incremental Client Assembly

The Builder pattern appears in packages/corsair/core/client.ts through buildCorsairClient and buildIntegrationKeys. These functions incrementally assemble client objects, adding endpoint methods, error handlers, and optional features without exposing construction details.

// src/packages/corsair/core/client.ts
export function buildCorsairClient<Plugins extends readonly CorsairPlugin[]>(
  plugins: Plugins,
  opts: {
    database?: CorsairDatabase;
    tenantId?: string;
    kek?: string;
    /* …other injected services */
  },
) {
  // Dynamically bind each plugin's endpoints onto the client instance
  // and wire up error handlers, key management, etc.
}

The builder enables feature toggling—tunnel support and connect loops attach only when configured—without complicating the public API.


Registry Pattern: Operation-to-Handler Mapping

Every plugin's endpoints folder implements the Registry pattern. In packages/slack/endpoints/types.ts and similar files, Corsair maintains maps of operation names to handler functions and schema descriptors.

// src/packages/slack/endpoints/types.ts
export const SlackEndpointInputSchemas = {
  channelsList: z.object({ /* … */ }),
};

export const SlackEndpointOutputSchemas = {
  channelsList: z.object({ channels: z.array(z.any()) }),
};
// src/packages/slack/index.ts
const slackEndpointsNested = {
  channels: {
    list: Channels.list,      // ← registry entry
  },
  users: { /* … */ },
};

export const Slack = {
  ...slackEndpointsNested,
};

This registry enables:

  • Compile-time validation through derived TypeScript types
  • Automated test coverage via registry coverage test suites
  • Dynamic dispatch at runtime for operation routing

Proxy Pattern: Configuration Guarding

The Proxy pattern in packages/corsair/core/auth/errors.ts provides runtime guards for missing configuration. The createMissingConfigProxy function throws informative errors when required resources—database or encryption key—are absent.

// src/packages/corsair/core/auth/errors.ts
export function createMissingConfigProxy<T>(hasDb: boolean, hasKek: boolean): T {
  return new Proxy({} as T, {
    get(_, prop) {
      if (!hasDb && prop === 'keys') {
        throw new Error('Database not configured – keys unavailable');
      }
      if (!hasKek && prop === 'keys') {
        throw new Error('Encryption key (kek) missing – keys unavailable');
      }
      return undefined;
    },
  });
}

This prevents null reference errors deep in the call stack and surfaces configuration problems immediately at the point of access.


Singleton Pattern: Unique Internal Configuration

Corsair implements Singleton semantics using a Symbol in packages/corsair/core/index.ts:

// src/packages/corsair/core/index.ts
const CORSAIR_INTERNAL = Symbol('corsair:internal');

The CORSAIR_INTERNAL Symbol guarantees a unique configuration object per integration. This prevents accidental duplication across module boundaries while remaining accessible to internal utilities that possess the Symbol reference.


Strategy/Adapter Pattern: Pluggable Error Handling

Each plugin's error-handlers.ts file implements the Strategy pattern, mapping API-specific error codes to uniform CorsairError types. This allows third-party service quirks to adapt to Corsair's common contract.

// src/packages/slack/error-handlers.ts
export const errorHandlers: Record<string, CorsairErrorHandler> = {
  'invalid_auth': () => new AuthMissingError('Slack token invalid'),
  'rate_limited': () => new RetryAfterError('Slack rate limit exceeded'),
};

The core executes these strategies polymorphically, treating all plugin errors uniformly regardless of origin service.


Dependency Injection: External Behavior Supply

Dependency Injection appears throughout createCorsair signatures. The function accepts a CorsairIntegration config object containing:

  • Plugin arrays
  • Database connections
  • Key encryption keys (KEK)
  • Hub configuration
  • Permission definitions

This makes the core implementation-agnostic and trivially testable with mock dependencies.


Facade Pattern: Simplified Public API

Each plugin exports a Facade that presents a flat, user-friendly interface. In packages/slack/index.ts, consumers interact with slack.channels.list rather than navigating registries, factories, or network layers directly.

This abstraction hides:

  • Endpoint registration mechanics
  • HTTP client instantiation
  • Schema validation pipelines
  • Retry and error-handling logic

How These Patterns Work Together

The design patterns in Corsair create a cohesive architecture with three primary benefits:

  1. Strong static typing — Types flow from registry definitions through factories to end consumers, catching errors at compile time.

  2. Automated verification — Registry-driven tests validate complete operation coverage without manual test enumeration.

  3. Runtime safety — Proxy guards and strategy objects catch configuration or API errors early with actionable messages.

New integrations require only: a factory function, endpoint registry entries, and schema registrations. The existing pattern infrastructure handles the rest.


Summary

  • Factory (createCorsair) centralizes integration construction in packages/corsair/core/index.ts

  • Builder (buildCorsairClient) assembles clients incrementally in packages/corsair/core/client.ts

  • Registry maps operations to handlers in every plugin's endpoints folder

  • Proxy guards missing configuration via createMissingConfigProxy in packages/corsair/core/auth/errors.ts

  • Singleton ensures unique internal state through the CORSAIR_INTERNAL Symbol

  • Strategy enables pluggable error handling in each plugin's error-handlers.ts

  • Dependency Injection makes the core agnostic of concrete implementations

  • Facade provides simplified APIs through exported plugin namespaces


Frequently Asked Questions

Does Corsair use dependency injection?

Yes. All createCorsair signatures accept a CorsairIntegration configuration object containing plugins, database connections, encryption keys, and other services. This external supply of dependencies makes the core completely independent of concrete implementations and enables comprehensive unit testing with mocks.

What is the CORSAIR_INTERNAL Symbol used for?

The CORSAIR_INTERNAL Symbol in packages/corsair/core/index.ts implements Singleton semantics. It guarantees exactly one internal configuration object exists per integration and prevents accidental duplication across module boundaries, while remaining accessible only to code that possesses the Symbol reference.

How does Corsair handle errors from different APIs uniformly?

Each plugin provides a error-handlers.ts file implementing the Strategy pattern. These files map service-specific error codes (like Slack's rate_limited) to common CorsairError types. The core executes these strategies polymorphically, treating all errors uniformly regardless of which third-party service originated them.

Why does Corsair use a Proxy for configuration validation?

The createMissingConfigProxy function in packages/corsair/core/auth/errors.ts throws clear, actionable errors when developers access features requiring unconfigured resources. This fails fast at the point of access rather than allowing null reference errors deep in the call stack, providing immediate feedback about missing database or encryption key configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →