Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Codefied-CodePix/Karokar-backend/llms.txt

Use this file to discover all available pages before exploring further.

KaroKar is structured as a Domain-Driven Modular Monolith — ten bounded contexts, each owning its own business rules, running inside a single deployable process. The inevitable challenge with any multi-domain system is cross-cutting workflows: a booking approval must update vehicle availability, trigger a notification, and create an audit record all at once. Without a deliberate communication strategy, this turns into a web of tightly coupled service-to-service calls that are hard to test, hard to extend, and nearly impossible to extract into independent services later. KaroKar solves this with an event-driven communication strategy: every significant business action results in a domain event, and other bounded contexts react to that event independently.

Core Principle

A domain owns its business rules. Other domains may react to those rules through events — they may never reach in and execute another domain’s business logic directly.
Domains communicate through facts, not commands.
Instead of BookingService calling AvailabilityService.reserve(), the Booking Domain says “A booking was approved” and the Availability Domain decides on its own what to do with that information.

The DomainEvent Base Class

Every event in KaroKar extends the same abstract DomainEvent class, guaranteeing a consistent envelope on all published facts.
// src/shared/domain/domain-event.ts
import { v4 as uuidv4 } from 'uuid';

export abstract class DomainEvent {
  readonly eventId: string;
  readonly occurredAt: Date;

  constructor(
    readonly eventType: string,
    readonly aggregateType: string,
    readonly aggregateId: string,
    readonly payload: Record<string, unknown>,
    readonly version: number = 1,
    eventId?: string,
    occurredAt?: Date,
  ) {
    this.eventId   = eventId    ?? uuidv4();
    this.occurredAt = occurredAt ?? new Date();
  }
}
FieldPurpose
eventIdGlobally unique identifier — used for idempotency checks
eventTypeHuman-readable event name, e.g. "BookingApproved"
aggregateTypeThe domain aggregate that produced the event, e.g. "Booking"
aggregateIdThe specific aggregate instance ID
occurredAtWall-clock timestamp of when the business fact occurred
versionSchema version — incremented when the payload shape changes
payloadEvent-specific data (typed by each concrete subclass)

A Real Event: BookingApprovedEvent

Concrete events are thin wrappers around DomainEvent. They set the fixed eventType and aggregateType values while accepting the domain-specific payload from the service layer.
// src/booking/domain/events/booking-approved.event.ts
import { DomainEvent } from '../../../shared/domain/domain-event';

export class BookingApprovedEvent extends DomainEvent {
  constructor(bookingId: string, payload: Record<string, unknown>) {
    super('BookingApproved', 'Booking', bookingId, payload);
  }
}
A published instance of this event would look like:
{
  "eventId":      "a3f2c1b0-...",
  "eventType":    "BookingApproved",
  "aggregateType":"Booking",
  "aggregateId":  "booking_456",
  "occurredAt":   "2026-06-02T10:00:00.000Z",
  "version":      1,
  "payload": {
    "bookingId": "booking_456",
    "vehicleId": "vehicle_123"
  }
}

Phase 01 Event Bus: NestJS EventEmitter

In Phase 01 KaroKar uses an in-process event bus — NestJS’s built-in EventEmitter2 module. Events are published and consumed inside the same application process with no network hop and no external broker. This is intentional: the current business scale does not justify the operational overhead of Kafka, RabbitMQ, or SNS/SQS, and the in-process bus is fully sufficient for the modular monolith architecture.

Producer → Consumer Flow

The canonical example is booking approval:
1

Booking Domain approves a booking

The BookingService completes its state transition and emits a BookingApprovedEvent onto the event bus.
2

Availability Domain reacts

An @OnEvent('BookingApproved') listener creates an availability block for the vehicle, preventing double-bookings.
3

Notification Domain reacts

A separate listener sends a confirmation email to the Corporate Admin and an alert to the Vendor Admin.
4

Audit Domain reacts

Another listener writes an immutable audit record: which user approved the booking, in which organization, at what timestamp.
Each consumer is completely independent — adding a new reaction (e.g., triggering an invoice) requires only a new listener, with zero changes to the Booking Domain.

Critical Domain Events

EventTriggered When
BookingRequestedA Corporate Admin creates a new booking request
BookingApprovedA Vendor Admin approves the request
BookingRejectedA Vendor Admin rejects the request
BookingAssignedA vehicle is assigned to the booking
BookingActivatedThe booking period begins
BookingCompletedThe booking period ends successfully
BookingCancelledCancelled before activation
BookingTerminatedForcefully closed mid-booking
EventTriggered When
AssignmentCreatedA Corporate Admin assigns a vehicle to an employee
AssignmentAcceptedThe employee accepts the assignment
AssignmentRejectedThe employee rejects the assignment
AssignmentClosedThe assignment period concludes
EventTriggered When
VehicleCreatedA new vehicle is registered by a Vendor
VehicleUpdatedVehicle details are modified
VehicleActivatedA vehicle is cleared for bookings
VehicleSuspendedA vehicle is taken out of service
VehicleMaintenanceScheduledMaintenance window is created
VehicleMaintenanceCompletedMaintenance window closes
EventTriggered When
VerificationRequestedAn entity submits documents for review
VerificationApprovedPlatform Admin approves the submission
VerificationRejectedPlatform Admin rejects the submission
VerificationExpiredA validity period elapses
VerificationRevokedA verification is cancelled due to fraud or compliance breach
EventTriggered When
OrganizationCreatedA new organization completes registration
OrganizationApprovedPlatform Admin approves the organization
OrganizationSuspendedPlatform Admin suspends the organization

Event Consumption Rules

Consumers must never modify producer domain state. A consumer reacting to BookingApproved may create availability records, send notifications, or write audit entries — but it must never reach back and update the Booking entity’s status. Only the Booking Domain controls Booking state. Violating this rule creates circular dependencies and breaks domain isolation.
Permitted consumer actions:
  • ✅ Update read models
  • ✅ Create audit records
  • ✅ Send notifications
  • ✅ Create or release availability records
  • ✅ Generate analytics
Forbidden consumer actions:
  • ❌ Modify the producer aggregate’s persistent state
  • ❌ Call the producer domain’s service methods

Event Idempotency

Consumers must be idempotent. Because the same event may be delivered more than once — particularly as the transport layer evolves toward distributed brokers — every consumer must guard against duplicate processing. A BookingApproved event processed twice must not create two availability blocks or send two emails. Consumers should use the eventId field to deduplicate events where necessary.

Event Versioning

Events are public contracts. When a payload must change, the version field is incremented. Consumers should remain backward compatible with the previous version wherever possible. Breaking version changes should be introduced as new event types rather than silent payload modifications.

Future Evolution Path

The event contracts are stable regardless of which transport layer carries them. Only the bus implementation changes across phases — the DomainEvent structure and the consumer handler signatures remain untouched.
1

Phase 01 — In-Process Event Bus

NestJS EventEmitter2 dispatches events synchronously within the application process. No external dependencies required.
2

Phase 02 — Transactional Outbox Pattern

Events are written to an outbox table within the same database transaction as the aggregate change, then relayed to a broker by a background worker. Eliminates the risk of a committed aggregate state and a dropped event.
3

Phase 03 — Distributed Message Broker

Kafka, SNS/SQS, or AWS EventBridge for multi-service fan-out, replay, and cross-region delivery. Individual bounded contexts can be extracted into separate deployable services at this stage.
Audit and Notification integrations are handled entirely through event subscriptions. No business domain should ever call an audit logger or send a notification directly — those concerns belong to the Audit Domain and Notification Domain respectively, reacting to the events they care about.

Build docs developers (and LLMs) love