Files
calendar/agents/rules/patterns-dependency-injection.md
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: [email protected] <[email protected]>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

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

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

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

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03: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.diy'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.diy Engineering Blog