Understanding Backend Architecture
Monolithic Architecture
When to Use Monoliths
- Small to medium-sized applications
- Teams with limited DevOps expertise
- Applications with simple deployment requirements
- Rapid prototyping and MVP development
Monolithic Structure
// Express.js Monolithic Structure
src/
├── controllers/ # Request handlers
├── services/ # Business logic
├── models/ # Data models
├── middleware/ # Custom middleware
├── routes/ # Route definitions
├── utils/ # Utility functions
└── app.ts # Application entry pointMicroservices Architecture
When to Use Microservices
- Large, complex applications
- Teams that can work independently
- Applications requiring different scaling patterns
- Systems with diverse technology requirements
Microservice Example
// User Service
const app = express();
app.use(express.json());
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
const userController = new UserController(userService);
app.get('/users/:id', userController.getUser);
app.post('/users', userController.createUser);
app.put('/users/:id', userController.updateUser);
app.delete('/users/:id', userController.deleteUser);
export default app;Domain-Driven Design (DDD)
Understanding DDD
Domain Model
// Domain Layer
export class User {
private constructor(
private readonly id: UserId,
private email: Email,
private name: string,
private status: UserStatus
) {}
static create(email: string, name: string): User {
return new User(
UserId.generate(),
new Email(email),
name,
UserStatus.ACTIVE
);
}
changeEmail(newEmail: string): void {
this.email = new Email(newEmail);
}
isActive(): boolean {
return this.status === UserStatus.ACTIVE;
}
}Repository Pattern
Repository Pattern
// Repository Interface
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// Concrete Implementation
class PostgresUserRepository implements UserRepository {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
const row = await this.db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return row ? this.toDomain(row) : null;
}
private toDomain(row: any): User {
return new User(
new UserId(row.id),
new Email(row.email),
row.name,
row.status
);
}
}CQRS (Command Query Responsibility Segregation)
CQRS Implementation
// Commands
class CreateUserCommand {
constructor(
public email: string,
public name: string
) {}
}
// Command Handler
class CreateUserCommandHandler {
constructor(
private userRepository: UserRepository,
private eventStore: EventStore
) {}
async handle(command: CreateUserCommand): Promise<void> {
const user = User.create(command.email, command.name);
await this.userRepository.save(user);
await this.eventStore.append(
user.getId(),
new UserCreatedEvent(user.getId(), user.getEmail())
);
}
}API Gateway Pattern
API Gateway
class ApiGateway {
constructor(
private userService: UserService,
private orderService: OrderService,
private productService: ProductService
) {}
async handleRequest(request: Request): Promise<Response> {
const { method, path } = request;
// Route to appropriate service
if (path.startsWith('/api/users')) {
return await this.userService.handleRequest(request);
} else if (path.startsWith('/api/orders')) {
return await this.orderService.handleRequest(request);
} else if (path.startsWith('/api/products')) {
return await this.productService.handleRequest(request);
}
return new Response('Not Found', { status: 404 });
}
}Best Practices
- Choose the Right Pattern - Start simple, refactor when needed
- Error Handling - Implement proper error handling
- Monitoring - Add health checks and metrics
- Security - Implement proper authentication and authorization
- Documentation - Document your APIs and architecture decisions