Files
calendar/agents/rules/patterns-dependency-injection.md
T
Keith WilliamsGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d4ea931bf8 feat(agents): add modular engineering rules from 2026 standards (#26847)
* feat(agents): add modular engineering rules from 2026 standards

Add a rules directory with individual rule files derived from the
Cal.com Engineering in 2026 and Beyond blog post. Rules are organized
by section (architecture, quality, data, api, performance, testing,
patterns, culture) following the Vercel agent-skills structure.

Includes:
- _sections.md defining rule categories and impact levels
- _template.md for creating new rules
- 14 individual rule files covering key engineering standards
- README documenting the rules structure and usage

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(agents): consolidate DI and Repository+DTO docs into rules

- Move di-pattern.md content to rules/patterns-di-pattern.md
- Extract Repository + DTO section from knowledge-base.md into:
  - rules/data-repository-methods.md (method naming conventions)
  - rules/data-dto-boundaries.md (DTO location and naming)
- Update knowledge-base.md to reference the new rule files
- Delete old di-pattern.md file

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(agents): remove stub reference sections from knowledge-base.md

The rules directory is self-contained with its own README, so these
redirect sections are unnecessary clutter.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(agents): combine DI pattern rules into single file

Merged patterns-di-pattern.md into patterns-dependency-injection.md
to eliminate overlap and create one comprehensive DI guide.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-16 10:57:28 +01:00

9.0 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Dependency Injection for Loose Coupling HIGH Enables build-time safety, testability, and maintainability patterns, dependency-injection, di, ioctopus, moduleloader, testing, coupling

Use Dependency Injection for Loose Coupling

Impact: HIGH

Dependency Injection enables loose coupling, facilitates testing, and isolates concerns. Dependencies should be injected via DI containers rather than instantiated directly within classes.

Incorrect (tight coupling with direct instantiation):

class BookingService {
  private repository = new BookingRepository();
  private emailService = new EmailService();
  private calendarService = new GoogleCalendarService();

  async createBooking(data: CreateBookingDTO) {
    const booking = await this.repository.create(data);
    await this.emailService.sendConfirmation(booking);
    await this.calendarService.createEvent(booking);
    return booking;
  }
}

Correct (dependency injection):

class BookingService {
  constructor(
    private readonly repository: BookingRepository,
    private readonly emailService: EmailService,
    private readonly calendarService: CalendarService,
  ) {}

  async createBooking(data: CreateBookingDTO) {
    const booking = await this.repository.create(data);
    await this.emailService.sendConfirmation(booking);
    await this.calendarService.createEvent(booking);
    return booking;
  }
}

Required patterns:

  • Application Services: Orchestrate use cases, coordinate between domain services and repositories
  • Domain Services: Contain business logic that doesn't naturally belong to a single entity
  • Repositories: Abstract data access, isolate technology choices
  • Caching Proxies: Wrap repositories or services to add caching behavior transparently
  • Decorators: Add cross-cutting concerns (logging, metrics) without polluting domain logic

Cal.com's Type-Safe DI with moduleLoader

We use @evyweb/ioctopus to manage service and repository dependencies. The moduleLoader pattern provides type-safe dependency injection, ensuring that if a service adds a new dependency, TypeScript will catch missing dependencies at build time rather than runtime.

Core Concepts

Tokens: Unique symbols that identify each service or repository in the DI container. Every injectable class needs a corresponding token. Tokens should be defined in a tokens.ts file within the feature's di/ directory.

Modules (.module.ts files): Define how classes are instantiated and what dependencies they require using the bindModuleToClassOnToken function. Each module exports a moduleLoader object that knows how to load itself and its dependencies.

Containers (.container.ts files): Create a container instance and expose getter functions that consumers use to obtain service instances. Containers use the moduleLoader to automatically load all required dependencies.

bindModuleToClassOnToken: A type-safe function that binds a class to a token and declares its dependencies. TypeScript ensures the declared dependencies match what the class constructor expects.

How It Works

Step 1: Create tokens in the feature's di directory

// packages/features/myfeature/di/tokens.ts
export const MY_FEATURE_DI_TOKENS = {
  MY_SERVICE: Symbol("MyService"),
  MY_SERVICE_MODULE: Symbol("MyServiceModule"),
};

Then import these tokens in the central tokens file:

// packages/features/di/tokens.ts
import { MY_FEATURE_DI_TOKENS } from "@calcom/features/myfeature/di/tokens";

export const DI_TOKENS = {
  // ...existing tokens
  ...MY_FEATURE_DI_TOKENS,
};

Step 2: Define the service class with constructor injection

For services with multiple dependencies, use a dependencies interface:

// packages/features/myfeature/services/MyService.ts
export interface IMyServiceDeps {
  bookingRepo: BookingRepository;
  userRepo: UserRepository;
}

export class MyService {
  constructor(private deps: IMyServiceDeps) {}

  async doSomething() {
    const bookings = await this.deps.bookingRepo.findMany({...});
    const user = await this.deps.userRepo.findById({...});
  }
}

For services/repositories with a single dependency, pass it directly:

// packages/features/myfeature/repositories/MyRepository.ts
export class MyRepository {
  constructor(private prismaClient: PrismaClient) {}

  async findById(id: string) {
    return this.prismaClient.myModel.findUnique({ where: { id } });
  }
}

Step 3: Create a module file with moduleLoader

// packages/features/myfeature/di/MyService.module.ts
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { MyService } from "@calcom/features/myfeature/services/MyService";

import { moduleLoader as bookingRepositoryModuleLoader } from "./BookingRepository.module";
import { moduleLoader as userRepositoryModuleLoader } from "./UserRepository.module";
import { MY_FEATURE_DI_TOKENS } from "./tokens";

const thisModule = createModule();
const token = MY_FEATURE_DI_TOKENS.MY_SERVICE;
const moduleToken = MY_FEATURE_DI_TOKENS.MY_SERVICE_MODULE;

const loadModule = bindModuleToClassOnToken({
  module: thisModule,
  moduleToken,
  token,
  classs: MyService,
  depsMap: {
    bookingRepo: bookingRepositoryModuleLoader,
    userRepo: userRepositoryModuleLoader,
  },
});

export const moduleLoader: ModuleLoader = {
  token,
  loadModule,
};

export type { MyService };

For a single dependency, use dep instead of depsMap:

// packages/features/myfeature/di/MyRepository.module.ts
const loadModule = bindModuleToClassOnToken({
  module: thisModule,
  moduleToken,
  token,
  classs: MyRepository,
  dep: prismaModuleLoader,
});

Step 4: Create a container that uses the moduleLoader

// packages/features/myfeature/di/MyService.container.ts
import { createContainer } from "@calcom/features/di/di";
import { type MyService, moduleLoader as myServiceModuleLoader } from "./MyService.module";

const myServiceContainer = createContainer();

export function getMyService(): MyService {
  myServiceModuleLoader.loadModule(myServiceContainer);
  return myServiceContainer.get<MyService>(myServiceModuleLoader.token);
}

Step 5: Use the service via the container's getter function

import { getMyService } from "@calcom/features/myfeature/di/MyService.container";

const myService = getMyService();
await myService.doSomething();

Common Mistakes to Avoid

Mistake 1: Creating a repository or service class with all static methods

// Bad - Static methods bypass DI
export class BookingRepository {
  static async findById(id: string) {
    return prisma.booking.findUnique({ where: { id } });
  }
}

// Good - Instance methods with constructor injection
export class BookingRepository {
  constructor(private prismaClient: PrismaClient) {}

  async findById(id: string) {
    return this.prismaClient.booking.findUnique({ where: { id } });
  }
}

Mistake 2: Manually instantiating a class instead of using the DI container

// Bad - Manual instantiation bypasses DI
const bookingRepo = new BookingRepository(prisma);
const myService = new MyService({ bookingRepo });

// Good - Use the DI container's getter function
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
const myService = getMyService();

Mistake 3: Importing Prisma directly in a service instead of using repository injection

// Bad - Service imports Prisma directly
import prisma from "@calcom/prisma";

export class MyService {
  async doSomething() {
    const bookings = await prisma.booking.findMany({...});
  }
}

// Good - Service depends on repository via DI
export class MyService {
  constructor(private deps: IMyServiceDeps) {}

  async doSomething() {
    const bookings = await this.deps.bookingRepo.findMany({...});
  }
}

Mistake 4: Manual module loading in containers (not type-safe)

// Bad - Manual module loading is not type-safe
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);

// Good - Use moduleLoader for type-safe dependency loading
export function getMyService(): MyService {
  myServiceModuleLoader.loadModule(container);
  return container.get<MyService>(myServiceModuleLoader.token);
}

Why Use DI?

  • Build-time safety: TypeScript catches missing dependencies before runtime
  • Testability: Dependencies can be easily mocked in tests
  • Consistency: All instances are created the same way with proper dependencies
  • Maintainability: Changing a dependency only requires updating the module binding
  • Automatic dependency resolution: The moduleLoader automatically loads all dependencies recursively
  • Self-documenting: Each module declares its own dependencies explicitly

Reference: Cal.com Engineering Blog