Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 207 additions & 1 deletion agents/knowledge-base.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,4 +730,210 @@ When doing logic that depends on Browser locale, use i18n.language (prefer to de

Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable.

The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs.
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It's a non-ideal solution to a problem that unfortunately exists due to Dayjs.

## Dependency Injection (DI) Pattern

We use a Dependency Injection pattern powered by `@evyweb/ioctopus` to manage service and repository dependencies. This pattern ensures proper dependency management, testability, and consistent instantiation of services throughout the codebase.

### Core Concepts

The DI system consists of three main components:

**Tokens** (`packages/features/di/tokens.ts`): Unique symbols that identify each service or repository in the DI container. Every injectable class needs a corresponding token.

**Modules** (`packages/features/di/modules/*.ts`): Define how classes are instantiated and what dependencies they require. Modules bind tokens to class implementations.

**Containers** (`packages/features/di/containers/*.ts`): Assemble modules together and expose getter functions that consumers use to obtain service instances.

### How It Works

Here's the flow using `BusyTimesService` as an example:

**Step 1: Define the service class with constructor injection**

```typescript
// packages/features/busyTimes/services/getBusyTimes.ts
export interface IBusyTimesService {
bookingRepo: BookingRepository;
}

export class BusyTimesService {
constructor(public readonly dependencies: IBusyTimesService) {}

async getBusyTimes(params: {...}) {
// Use dependencies via this.dependencies.bookingRepo
const bookings = await this.dependencies.bookingRepo.findAllExistingBookingsForEventTypeBetween({...});
// ...
}
}
```

**Step 2: Create a module that binds the service to its token**

```typescript
// packages/features/di/modules/BusyTimes.ts
import { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes";
import { createModule } from "../di";
import { DI_TOKENS } from "../tokens";

export const busyTimesModule = createModule();
busyTimesModule.bind(DI_TOKENS.BUSY_TIMES_SERVICE).toClass(BusyTimesService, {
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
} satisfies Record<keyof IBusyTimesService, symbol>);
```

**Step 3: Create a container that loads all required modules and exposes a getter**

```typescript
// packages/features/di/containers/BusyTimes.ts
import type { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes";
import { DI_TOKENS } from "@calcom/features/di/tokens";
import { prismaModule } from "@calcom/features/di/modules/Prisma";
import { createContainer } from "../di";
import { bookingRepositoryModule } from "../modules/Booking";
import { busyTimesModule } from "../modules/BusyTimes";

const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);

export function getBusyTimesService() {
return container.get<BusyTimesService>(DI_TOKENS.BUSY_TIMES_SERVICE);
}
```

**Step 4: Use the service via the container's getter function**

```typescript
// Anywhere in the codebase
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";

const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({...});
```

### Common Mistakes to Avoid

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

Static methods bypass the DI system entirely, making the code harder to test and breaking the dependency chain.

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

static async create(data: BookingCreateInput) {
return prisma.booking.create({ data });
}
}

// Usage (wrong - no DI)
const booking = await BookingRepository.findById("123");
```

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

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

async create(data: BookingCreateInput) {
return this.prismaClient.booking.create({ data });
}
}

// Usage (correct - via DI container)
const bookingRepo = getBookingRepository();
const booking = await bookingRepo.findById("123");
```

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

Even if you define a class with constructor injection, manually calling `new` bypasses the DI system and its benefits.

```typescript
// ❌ Bad - Manual instantiation bypasses DI
import { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import prisma from "@calcom/prisma";

// Wrong: manually creating instances
const bookingRepo = new BookingRepository(prisma);
const busyTimesService = new BusyTimesService({ bookingRepo });
const busyTimes = await busyTimesService.getBusyTimes({...});
```

```typescript
// ✅ Good - Use the DI container's getter function
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";

// Correct: let the container manage instantiation
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({...});
```

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

Services should depend on repositories, not directly on Prisma. This maintains proper separation of concerns.

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

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

```typescript
// ✅ Good - Service depends on repository via DI
export interface IMyService {
bookingRepo: BookingRepository;
}

export class MyService {
constructor(public readonly dependencies: IMyService) {}

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

### Adding a New Service to the DI System

When creating a new service or repository that should use DI:

1. **Add tokens** to `packages/features/di/tokens.ts`:
```typescript
export const DI_TOKENS = {
// ...existing tokens
MY_SERVICE: Symbol("MyService"),
MY_SERVICE_MODULE: Symbol("MyServiceModule"),
};
```

2. **Create the service class** with a dependencies interface and constructor injection.

3. **Create a module** in `packages/features/di/modules/MyService.ts` that binds the service to its token.

4. **Create a container** in `packages/features/di/containers/MyService.ts` that loads all required modules and exports a getter function.

5. **Use the getter function** everywhere you need the service - never manually instantiate.

### Why Use DI?

- **Testability**: Dependencies can be easily mocked in tests by providing alternative implementations
- **Consistency**: All instances are created the same way with proper dependencies
- **Maintainability**: Changing a dependency only requires updating the module binding, not every usage site
- **Explicit dependencies**: The dependency graph is clear and documented in the module definitions
Loading