Design Patterns
Design patterns and architectural principles used in CodeNotify Server.
Overview
CodeNotify follows industry-standard design patterns to ensure maintainability, scalability, and testability.
Core Patterns
1. Adapter Pattern
Purpose: Abstract platform-specific implementations
Location: src/integrations/platforms/
Implementation:
// Interface
interface PlatformAdapter {
platformName: string;
enabled: boolean;
fetchContests(): Promise<ContestData[]>;
healthCheck(): Promise<boolean>;
}
// Base class
abstract class BasePlatformAdapter implements PlatformAdapter {
abstract platformName: string;
abstract enabled: boolean;
protected async makeRequest(url: string): Promise<any> {
// Common HTTP logic with retry
}
protected abstract transformToInternalFormat(data: any): ContestData[];
}
// Concrete implementation
class CodeforcesAdapter extends BasePlatformAdapter {
platformName = 'codeforces';
enabled = true;
async fetchContests(): Promise<ContestData[]> {
const data = await this.makeRequest('https://codeforces.com/api/contest.list');
return this.transformToInternalFormat(data.result);
}
protected transformToInternalFormat(contests: any[]): ContestData[] {
return contests.map(c => ({
platformId: c.id.toString(),
name: c.name,
platform: 'codeforces',
// ... transform fields
}));
}
}Benefits:
- Easy to add new platforms
- Consistent interface
- Platform-specific logic isolated
- Testable in isolation
2. Dependency Injection
Purpose: Loose coupling and testability
Implementation:
// Service with injected dependencies
@Injectable()
export class ContestsService {
constructor(
@InjectModel(Contest.name) private contestModel: Model<ContestDocument>,
@Inject(PLATFORM_ADAPTERS) private adapters: PlatformAdapter[],
private notificationsService: NotificationsService,
) {}
async syncPlatform(platform: string): Promise<void> {
const adapter = this.adapters.find(a => a.platformName === platform);
const contests = await adapter.fetchContests();
await this.upsertContests(contests);
}
}
// Module configuration
@Module({
providers: [
ContestsService,
{
provide: PLATFORM_ADAPTERS,
useFactory: () => [
new CodeforcesAdapter(),
new LeetCodeAdapter(),
new CodeChefAdapter(),
new AtCoderAdapter(),
],
},
],
})
export class ContestsModule {}Benefits:
- Testable with mocks
- Flexible configuration
- Clear dependencies
- Easier refactoring
3. Repository Pattern
Purpose: Abstract data access layer
Implementation:
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
// Repository methods
async findById(id: string): Promise<User> {
return this.userModel.findById(id);
}
async findByEmail(email: string): Promise<User> {
return this.userModel.findOne({ email });
}
async create(userData: CreateUserDto): Promise<User> {
const user = new this.userModel(userData);
return user.save();
}
async update(id: string, updates: Partial<User>): Promise<User> {
return this.userModel.findByIdAndUpdate(id, updates, { new: true });
}
}Benefits:
- Database agnostic
- Centralized queries
- Easy to test
- Consistent API
4. Guard Pattern
Purpose: Declarative route protection
Implementation:
// JWT Guard
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
// Roles Guard
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some(role => user.role === role);
}
}
// Usage
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Delete(':id')
async deleteUser(@Param('id') id: string) {
// Only admins can access
}Benefits:
- Declarative security
- Reusable guards
- Clear authorization
- Easy to test
5. Strategy Pattern
Purpose: Interchangeable algorithms
Implementation:
// Passport JWT Strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return {
id: payload.sub,
email: payload.email,
role: payload.role,
};
}
}Benefits:
- Pluggable authentication
- Multiple strategies possible
- Clean separation
- Framework integration
6. Factory Pattern
Purpose: Object creation logic
Implementation:
// Platform adapter factory
export const PLATFORM_ADAPTERS = 'PLATFORM_ADAPTERS';
@Module({
providers: [
{
provide: PLATFORM_ADAPTERS,
useFactory: (config: ConfigService) => {
const adapters = [];
if (config.get('CODEFORCES_ENABLED')) {
adapters.push(new CodeforcesAdapter());
}
if (config.get('LEETCODE_ENABLED')) {
adapters.push(new LeetCodeAdapter());
}
return adapters;
},
inject: [ConfigService],
},
],
})
export class PlatformsModule {}Benefits:
- Conditional creation
- Configuration-driven
- Centralized logic
- Easy to extend
7. Decorator Pattern
Purpose: Add behavior without modifying code
Implementation:
// Custom decorators
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
export const Public = () => SetMetadata('isPublic', true);
// Usage
@Public()
@Get('health')
healthCheck() {
return { status: 'ok' };
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: UserDocument) {
return user;
}Benefits:
- Clean syntax
- Reusable metadata
- Framework integration
- Type-safe
Architectural Principles
SOLID Principles
Single Responsibility
Each class has one reason to change:
// ✅ Good - Single responsibility
class ContestsService {
// Only handles contest business logic
}
class ContestSchedulerService {
// Only handles scheduled jobs
}
class NotificationsService {
// Only handles notifications
}Open/Closed
Open for extension, closed for modification:
// ✅ Good - Extend via new adapter
class NewPlatformAdapter extends BasePlatformAdapter {
// Add new platform without modifying existing code
}Liskov Substitution
Subtypes must be substitutable:
// ✅ Good - All adapters work the same way
function syncPlatform(adapter: PlatformAdapter) {
const contests = await adapter.fetchContests();
// Works with any adapter
}Interface Segregation
Clients shouldn't depend on unused interfaces:
// ✅ Good - Specific interfaces
interface ContestReader {
findById(id: string): Promise<Contest>;
findAll(): Promise<Contest[]>;
}
interface ContestWriter {
create(data: CreateContestDto): Promise<Contest>;
update(id: string, data: UpdateContestDto): Promise<Contest>;
}Dependency Inversion
Depend on abstractions, not concretions:
// ✅ Good - Depend on interface
class ContestsService {
constructor(
@Inject(PLATFORM_ADAPTERS) private adapters: PlatformAdapter[], // Interface
) {}
}DRY (Don't Repeat Yourself)
// ❌ Bad - Repeated logic
class CodeforcesAdapter {
async fetchContests() {
try {
const response = await axios.get(url, { timeout: 15000 });
return response.data;
} catch (error) {
// Retry logic...
}
}
}
// ✅ Good - Shared in base class
abstract class BasePlatformAdapter {
protected async makeRequest(url: string) {
// Common HTTP logic with retry
}
}Separation of Concerns
// ✅ Good - Separated layers
@Controller('contests') // HTTP layer
export class ContestsController {
constructor(private service: ContestsService) {}
@Get()
async getAll() {
return this.service.findAll(); // Delegate to service
}
}
@Injectable() // Business logic layer
export class ContestsService {
constructor(private contestModel: Model<Contest>) {}
async findAll() {
return this.contestModel.find(); // Delegate to data layer
}
}Best Practices
✅ Do
- Use interfaces for contracts
- Inject dependencies via constructor
- Keep classes focused (SRP)
- Favor composition over inheritance
- Write testable code
- Use TypeScript features
- Follow naming conventions
❌ Don't
- Don't use global state
- Don't tightly couple modules
- Don't skip error handling
- Don't ignore types
- Don't mix concerns
- Don't hardcode values
- Don't skip tests

