Training Guide
📈 Progressive Development Approach
Start Simple → Add Complexity → Master Architecture
🎯 Role: Product architect at Adit
💼 Experience: 5+ years in microservices architecture
🔧 Expertise: Event Sourcing, CQRS, Domain-Driven Design
📚 Focus: Building scalable, maintainable systems
🎓 Mission: Empowering teams to build better software
"My goal is to help you understand not just the 'how' but the 'why' behind our architecture decisions"
Use video controls to play/pause
Designed 50+ microservices
10x improvement in API response times
Trained 100+ developers
By the end of this training, you'll be able to independently create, test, and deploy microservices following our architecture patterns.
Business logic first
Binary protocol, type-safe
Every change is tracked
gRPC is a high-performance RPC (Remote Procedure Call) framework. Think of it as:
gRPC is up to 10x faster than REST APIs!
Proto files define your API contract. They specify:
syntax = "proto3";
package patient;
// Service definition
service PatientSrv {
// Method definition
rpc PatientDelete(PatientDeleteRequest) returns (PatientDeleteResponse);
}
// Request message
message PatientDeleteRequest {
string patientId = 1;
string organizationId = 2;
string deletedBy = 3;
}
// Response message
message PatientDeleteResponse {
bool success = 1;
string patientId = 2;
optional string error = 3;
}
Proto file defines the API contract. Service and method names here will be used in @GrpcMethod.
// In your controller:
@GrpcMethod('PatientSrv', 'PatientDelete') // Must match proto!
async patientDelete(payload: PatientDeleteRequest) {
// payload will have: patientId, organizationId, deletedBy
}
Always create your proto file FIRST, then implement the code to match it!
A task represents one specific operation or endpoint in your service. For example:
patient-create_task
= Creating a new patientpatient-update_task
= Updating patient informationpatient-getbyid_task
= Fetching a specific patientpatient-delete_task
= Deleting a patientEach task is self-contained with its own business logic, data access, and API endpoint.
One task = One operation
All related code in one place
Isolated functionality
Business logic (domain) is separate from infrastructure (application)
Each layer can be tested independently
Easy to find and modify specific functionality
New features don't affect existing code
The domain folder contains your pure business logic. This is the heart of your application.
Aggregate: Main business entity
Domain Model: Base properties
Records of things that happened
(PatientCreated, PatientDeleted)
Strongly typed concepts
(Email, PhoneNumber, PatientId)
Business rule violations
(PatientAlreadyExists, InvalidAge)
The application folder orchestrates the domain logic and handles infrastructure concerns.
imp/ Command definitions
handler/ Execution logic
imp/ Query definitions
handler/ Query execution
Data access layer
Loads and saves aggregates
Data Transfer Objects
Request/Response shapes
We'll build features in three phases to make learning easier:
Goal: Get something working quickly
✅ Result: A working endpoint you can test!
Goal: Improve architecture with CQRS
✅ Result: Better separation of concerns!
Goal: Complete audit trail
✅ Result: Full history of all changes!
Without it, your components won't be registered!
The @Adit decorator registers your components with the framework so they can be:
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterRepository',
})
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterCommandHandler',
})
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterEvent',
})
Cause: Missing @Adit decorator
export class PatientDeleteTaskCommandHandler {
// Handler won't be found!
}
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterCommandHandler',
})
export class PatientDeleteTaskCommandHandler {
// Handler will work!
}
No @Adit = Component not registered = Errors!
Great job! Take a break and recharge.
See you back at 2:30 PM for the afternoon session!
💡 Tip: Review what we've learned so far and prepare your questions
Let's create a new endpoint to delete a patient using our progressive approach.
File:
patient-delete_task/proto/patient-delete.task.proto
syntax = "proto3";
package patient;
service PatientSrv {
rpc PatientDelete(PatientDeleteRequest) returns (PatientDeleteResponse);
}
message PatientDeleteRequest {
string patientId = 1;
string organizationId = 2;
string deletedBy = 3;
}
message PatientDeleteResponse {
bool success = 1;
string patientId = 2;
optional string error = 3;
}
Proto file defines the API contract. Service and method names here will be used in @GrpcMethod.
cd apps/patient_srv/src/
mkdir -p patient-delete_task/proto
mkdir -p patient-delete_task/domain/models
mkdir -p patient-delete_task/application/repositories
# We'll create more folders later as we need them
File:
patient-delete_task/domain/models/patient-delete.task.aggregate.ts
import { AggregateRoot } from '@adit/core/event';
export class PatientDeleteTaskAggregate extends AggregateRoot {
private isDeleted: boolean = false;
private deletedBy?: string;
private deletedAt?: Date;
// Simple business logic method (no events yet)
markAsDeleted(deletedBy: string): void {
if (this.getIsDeleted()) {
throw new Error('Patient is already deleted');
}
// For now, just update the state directly
this.isDeleted = true;
this.deletedBy = deletedBy;
this.deletedAt = new Date();
}
// Helper method to check status
getIsDeleted(): boolean {
return this.isDeleted;
}
}
File:
patient-delete_task/application/repositories/patient-delete.task.repository.ts
import { Adit } from '@adit/decorators';
import { AditService } from '@adit/util';
import { BaseMultitenantRepository } from '@adit/core/common';
import { PatientDeleteTaskAggregate } from '../../domain/models/patient-delete.task.aggregate';
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterRepository',
})
export class PatientDeleteTaskRepository extends BaseMultitenantRepository<PatientDeleteTaskAggregate> {
constructor() {
super(PatientDeleteTaskAggregate);
}
async findById(patientId: string): Promise<PatientDeleteTaskAggregate | null> {
// For now, let's create a mock patient
// In real code, this would load from database
const patient = new PatientDeleteTaskAggregate();
patient.id = patientId;
return patient;
}
async save(patient: PatientDeleteTaskAggregate): Promise<void> {
// For now, just log
console.log('Saving patient:', patient.id, 'isDeleted:', patient.getIsDeleted());
// In real code, this would save to database
}
}
import { Module } from '@nestjs/common';
import { PatientDeleteTaskController } from './patient-delete.task.controller';
import { PatientDeleteTaskService } from './patient-delete.task.service';
import { PatientDeleteTaskRepository } from './application/repositories/patient-delete.task.repository';
@Module({
controllers: [PatientDeleteTaskController],
providers: [
PatientDeleteTaskService,
PatientDeleteTaskRepository, // Add repository
],
})
export class PatientDeleteTaskModule {}
import { Injectable } from '@nestjs/common';
import { PatientDeleteTaskRepository } from './application/repositories/patient-delete.task.repository';
@Injectable()
export class PatientDeleteTaskService {
constructor(
private readonly repository: PatientDeleteTaskRepository,
) {}
async deletePatient(payload: any) {
console.log('=== SERVICE: Processing delete ===');
// Validate required fields
if (!payload.patientId || !payload.organizationId) {
throw new Error('Missing required fields');
}
// Load patient
const patient = await this.repository.findById(payload.patientId);
if (!patient) {
throw new Error('Patient not found');
}
// Apply business logic
patient.markAsDeleted(payload.deletedBy || 'system');
// Save changes
await this.repository.save(patient);
console.log('=== Delete completed ===');
return { success: true, patientId: payload.patientId };
}
}
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { PatientDeleteTaskService } from './patient-delete.task.service';
@Controller()
export class PatientDeleteTaskController {
constructor(private readonly service: PatientDeleteTaskService) {}
@GrpcMethod('PatientSrv', 'PatientDelete')
async patientDelete(payload: any) {
console.log('=== DELETE REQUEST RECEIVED ===', payload);
return this.service.deletePatient(payload);
}
}
File: apps/patient_srv/src/patient.module.ts
import { PatientDeleteTaskModule } from './patient-delete_task/patient-delete.task.module';
@Module({
imports: [
// ... other modules
PatientDeleteTaskModule, // Add this line
],
})
export class PatientModule {}
npm run start:patient_srv:dev
This works! But let's improve the architecture...
📚 Homework: Try creating another simple endpoint using Phase 1 approach
See you tomorrow at 10:00 AM for Day 2!
Now we'll refactor to use the Command pattern. This separates concerns better.
mkdir -p patient-delete_task/application/commands/imp
mkdir -p patient-delete_task/application/commands/handler
File:
patient-delete_task/application/commands/imp/patient-delete.command.ts
import { ICommand } from '@adit/core/event';
export class PatientDeleteCommand implements ICommand {
constructor(
public readonly patientId: string,
public readonly organizationId: string,
public readonly deletedBy: string,
) {}
}
A data structure representing an instruction to change something
Separates the "what" from the "how" - better testing & organization
File:
patient-delete_task/application/commands/handler/patient-delete.task.command.handler.ts
import { Adit } from '@adit/decorators';
import { AditService } from '@adit/util';
import { ICommandHandler } from '@adit/core/event';
import { PatientDeleteCommand } from '../imp/patient-delete.command';
import { PatientDeleteTaskRepository } from '../../repositories/patient-delete.task.repository';
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterCommandHandler',
})
export class PatientDeleteTaskCommandHandler implements ICommandHandler<PatientDeleteCommand> {
constructor(
private readonly repository: PatientDeleteTaskRepository,
) {}
async execute(command: PatientDeleteCommand): Promise<any> {
console.log('=== COMMAND HANDLER: Deleting patient ===');
// Step 1: Load the patient aggregate
const patient = await this.repository.findById(command.patientId);
if (!patient) {
throw new Error('Patient not found');
}
// Step 2: Call domain method to delete
patient.markAsDeleted(command.deletedBy);
// Step 3: Save the changes
await this.repository.save(patient);
return { success: true };
}
}
import { Injectable } from '@nestjs/common';
import { PatientBaseService } from '../patient-base.service';
import { CommandBus } from '@adit/core/event';
import { PatientDeleteCommand } from './application/commands/imp/patient-delete.command';
@Injectable()
export class PatientDeleteTaskService extends PatientBaseService {
constructor(protected readonly commandBus: CommandBus) {
super(commandBus);
}
async deletePatient(payload: any) {
console.log('=== SERVICE: Processing delete ===');
// Validate required fields
this.validateRequired(payload, ['patientId', 'organizationId']);
// Create and execute command
const command = new PatientDeleteCommand(
payload.patientId,
payload.organizationId,
payload.deletedBy || 'system'
);
await this.commandBus.execute(command);
console.log('=== Delete completed ===');
return { success: true, patientId: payload.patientId };
}
}
import { Module } from '@nestjs/common';
import { PatientDeleteTaskController } from './patient-delete.task.controller';
import { PatientDeleteTaskService } from './patient-delete.task.service';
import { PatientDeleteTaskRepository } from './application/repositories/patient-delete.task.repository';
import { PatientDeleteTaskCommandHandler } from './application/commands/handler/patient-delete.task.command.handler';
@Module({
controllers: [PatientDeleteTaskController],
providers: [
PatientDeleteTaskService,
PatientDeleteTaskRepository,
PatientDeleteTaskCommandHandler, // Add command handler
],
})
export class PatientDeleteTaskModule {}
Finally, let's add events to track all changes.
mkdir -p patient-delete_task/domain/events/imp
File:
patient-delete_task/domain/events/imp/patient-deleted.event.ts
import { Adit } from '@adit/decorators';
import { AditService } from '@adit/util';
import { Event, IEvent } from '@adit/core/event';
@Adit({
srvName: AditService.SrvNames.PATIENT_SRV,
type: 'RegisterEvent',
})
@Event('PATIENT_DELETED')
export class PatientDeletedEvent implements IEvent {
constructor(
public readonly data: {
patientId: string;
deletedBy: string;
deletedAt: Date;
}
) {}
}
Instead of storing just the current state, we store all events that led to that state. This gives us a complete audit trail!
File:
patient-delete_task/domain/models/patient-delete.task.aggregate.ts
import { AggregateRoot, EventHandler } from '@adit/core/event';
import { PatientDeletedEvent } from '../events/imp/patient-deleted.event';
export class PatientDeleteTaskAggregate extends AggregateRoot {
private isDeleted: boolean = false;
private deletedBy?: string;
private deletedAt?: Date;
// Updated to use events
markAsDeleted(deletedBy: string): void {
if (this.isDeleted) {
throw new Error('Patient is already deleted');
}
// Now we apply an event instead of direct state change
this.apply(new PatientDeletedEvent({
patientId: this.id,
deletedBy: deletedBy,
deletedAt: new Date(),
}));
}
// Event handler updates the state
@EventHandler(PatientDeletedEvent)
onPatientDeleted(event: PatientDeletedEvent): void {
this.isDeleted = true;
this.deletedBy = event.data.deletedBy;
this.deletedAt = event.data.deletedAt;
}
getIsDeleted(): boolean {
return this.isDeleted;
}
}
Excellent progress with advanced patterns!
Final session starts at 2:30 PM!
💡 We've covered Phases 2 & 3 - Command Pattern & Event Sourcing!
SELECT * FROM event_store WHERE aggregate_id = 'patient-123';
┌──────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Controller │────▶│ Service │────▶│ Repository │────▶│ Domain │ │ (gRPC) │ │ (Validates) │ │ (Data Access)│ │ (Business) │ └──────────────┘ └─────────────┘ └──────────────┘ └─────────────┘
┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │ Controller │────▶│ Service │────▶│ Command │ └──────────────┘ └─────────────┘ └─────────────┘ │ ▼ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │ Domain │◀────│ Repository │◀────│ Handler │ └──────────────┘ └─────────────┘ └─────────────┘
┌──────────────┐ ┌─────────────┐ │ Domain │────▶│ Event │ │ (Aggregate) │ └─────────────┘ └──────────────┘ │ ▼ ┌─────────────┐ │ Event Store │ └─────────────┘
Solution: Make sure you:
Solution: Check:
Solution: Make sure:
Solution: Check:
Real-world patterns we've implemented and tested in production environments
These patterns have been refined through real production deployments, handling thousands of requests per second
Take time to understand each phase. Better to build right than to rebuild.
🌟
You're now ready to build features in the Adit API!
- Helen Keller
Every expert was once a beginner. Don't hesitate to ask questions!
Use video controls to play/pause
Your success is our success!
Feel free to reach out with any questions or for code reviews!