Software architecture patterns and alternatives expert. Covers monolith vs microservices, serverless, event-driven, CQRS, hexagonal architecture, clean architecture, DDD, and architecture decision frameworks. Activates for architecture patterns, monolith, microservices, serverless, event-driven, CQRS, hexagonal architecture, clean architecture, DDD, architecture decisions, system design, scalability patterns.
Limited to specific tools
Additional assets for this skill
This skill is limited to using the following tools:
Expert in software architecture patterns, their tradeoffs, and when to use each approach. Helps teams make informed architecture decisions based on context, scale, and organizational constraints.
Definition: Single deployable unit containing all application logic.
Strengths:
Weaknesses:
Best For:
Example:
// Monolithic e-commerce app
class EcommerceApp {
constructor(
private userService: UserService,
private productService: ProductService,
private orderService: OrderService,
private paymentService: PaymentService
) {}
async checkout(userId: string, cartItems: CartItem[]) {
// All in-process, single transaction
const user = await this.userService.getUser(userId);
const products = await this.productService.validateStock(cartItems);
const order = await this.orderService.createOrder(user, products);
const payment = await this.paymentService.processPayment(order);
return { order, payment };
}
}
Definition: System composed of independently deployable services, each owning its domain.
Strengths:
Weaknesses:
Best For:
Example:
// Microservices e-commerce (order service)
class OrderService {
async createOrder(userId: string, items: CartItem[]) {
// 1. Call User Service via HTTP/gRPC
const user = await this.httpClient.get(`http://user-service/users/${userId}`);
// 2. Call Product Service to validate stock
const stockCheck = await this.httpClient.post('http://product-service/validate', { items });
// 3. Create order locally
const order = await this.orderRepo.create({ userId, items, total: stockCheck.total });
// 4. Publish OrderCreated event (eventual consistency)
await this.eventBus.publish('OrderCreated', { orderId: order.id, items });
return order;
}
}
Decision Matrix:
| Factor | Monolith | Microservices |
|---|---|---|
| Team Size | < 15 | 50+ |
| Domain Complexity | Low-Medium | High |
| Deployment Frequency | Weekly | Multiple/day |
| Scalability Needs | Uniform | Heterogeneous |
| Ops Maturity | Basic | Advanced |
Definition: Application logic runs in managed, ephemeral compute environments (FaaS - Function as a Service).
Patterns:
// AWS Lambda example
export const handler = async (event: S3Event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = record.s3.object.key;
// Process uploaded image
const image = await s3.getObject({ Bucket: bucket, Key: key });
const thumbnail = await generateThumbnail(image);
await s3.putObject({ Bucket: `${bucket}-thumbnails`, Key: key, Body: thumbnail });
}
};
// API Gateway Lambda handler
export const handler = async (event: APIGatewayProxyEvent) => {
const userId = event.pathParameters?.userId;
const user = await dynamoDB.get({ TableName: 'Users', Key: { id: userId } });
return {
statusCode: 200,
body: JSON.stringify(user),
};
};
Strengths:
Weaknesses:
Best For:
Anti-Patterns:
Definition: System components communicate through events rather than direct calls.
Patterns:
// Store events, not current state
type AccountEvent =
| { type: 'AccountOpened'; accountId: string; initialBalance: number }
| { type: 'MoneyDeposited'; accountId: string; amount: number }
| { type: 'MoneyWithdrawn'; accountId: string; amount: number };
class BankAccount {
private balance = 0;
// Replay events to reconstruct state
applyEvent(event: AccountEvent) {
switch (event.type) {
case 'AccountOpened':
this.balance = event.initialBalance;
break;
case 'MoneyDeposited':
this.balance += event.amount;
break;
case 'MoneyWithdrawn':
this.balance -= event.amount;
break;
}
}
// Commands produce events
withdraw(amount: number): AccountEvent[] {
if (this.balance < amount) throw new Error('Insufficient funds');
return [{ type: 'MoneyWithdrawn', accountId: this.id, amount }];
}
}
// Separate read and write models
// WRITE MODEL (Commands)
class OrderCommandHandler {
async createOrder(command: CreateOrderCommand) {
const order = new Order(command.userId, command.items);
await this.orderWriteRepo.save(order);
await this.eventBus.publish('OrderCreated', order.toEvent());
}
}
// READ MODEL (Queries - optimized for reads)
class OrderQueryHandler {
async getUserOrders(userId: string): Promise<OrderSummary[]> {
// Denormalized view, optimized for fast reads
return this.orderReadRepo.findByUserId(userId);
}
}
// Event handler updates read model asynchronously
class OrderEventHandler {
async onOrderCreated(event: OrderCreatedEvent) {
// Update denormalized read model
await this.orderReadRepo.insertSummary({
orderId: event.orderId,
userId: event.userId,
total: event.total,
status: 'pending',
});
}
}
Strengths:
Weaknesses:
Best For:
Definition: Application core is independent of external concerns (frameworks, databases, UI).
// DOMAIN (Core - no external dependencies)
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
class Order {
constructor(
public readonly id: string,
public readonly items: OrderItem[],
public status: OrderStatus
) {}
// Business logic here, independent of infrastructure
complete() {
if (this.status !== 'pending') throw new Error('Order already completed');
this.status = 'completed';
}
}
// APPLICATION (Use Cases)
class CompleteOrderUseCase {
constructor(private orderRepo: OrderRepository) {}
async execute(orderId: string) {
const order = await this.orderRepo.findById(orderId);
if (!order) throw new Error('Order not found');
order.complete();
await this.orderRepo.save(order);
}
}
// INFRASTRUCTURE (Adapters)
class PostgresOrderRepository implements OrderRepository {
async save(order: Order) {
await this.db.query('UPDATE orders SET status = $1 WHERE id = $2', [order.status, order.id]);
}
async findById(id: string): Promise<Order | null> {
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
if (!row) return null;
return new Order(row.id, row.items, row.status);
}
}
// UI (Adapter)
class ExpressOrderController {
constructor(private completeOrder: CompleteOrderUseCase) {}
async handleCompleteOrder(req: Request, res: Response) {
try {
await this.completeOrder.execute(req.params.orderId);
res.status(200).send({ message: 'Order completed' });
} catch (error) {
res.status(400).send({ error: error.message });
}
}
}
Strengths:
Weaknesses:
Best For:
Definition: Layered architecture with dependency inversion - dependencies point inward.
Layers (outermost to innermost):
Dependency Rule: Inner layers NEVER depend on outer layers.
// ENTITIES (innermost layer)
class User {
constructor(public id: string, public email: string, public passwordHash: string) {}
validatePassword(plainPassword: string, hasher: PasswordHasher): boolean {
return hasher.compare(plainPassword, this.passwordHash);
}
}
// USE CASES (application layer)
interface UserRepository {
findByEmail(email: string): Promise<User | null>;
}
class LoginUseCase {
constructor(
private userRepo: UserRepository,
private passwordHasher: PasswordHasher,
private tokenGenerator: TokenGenerator
) {}
async execute(email: string, password: string): Promise<{ token: string }> {
const user = await this.userRepo.findByEmail(email);
if (!user || !user.validatePassword(password, this.passwordHasher)) {
throw new Error('Invalid credentials');
}
const token = this.tokenGenerator.generate(user.id);
return { token };
}
}
// INTERFACE ADAPTERS (controllers)
class AuthController {
constructor(private loginUseCase: LoginUseCase) {}
async login(req: Request, res: Response) {
const { email, password } = req.body;
const result = await this.loginUseCase.execute(email, password);
res.json(result);
}
}
// FRAMEWORKS & DRIVERS (infrastructure)
class BcryptPasswordHasher implements PasswordHasher {
compare(plain: string, hash: string): boolean {
return bcrypt.compareSync(plain, hash);
}
}
class PostgresUserRepository implements UserRepository {
async findByEmail(email: string): Promise<User | null> {
const row = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
if (!row) return null;
return new User(row.id, row.email, row.password_hash);
}
}
Strengths:
Weaknesses:
Best For:
Strategic Design Patterns:
// SALES CONTEXT
class Customer {
constructor(public id: string, public creditLimit: number) {}
}
// SUPPORT CONTEXT (different model for same entity!)
class Customer {
constructor(public id: string, public supportTier: 'basic' | 'premium') {}
}
// Each context has its own model, even for same real-world concept
Sales Context → Customer Context (Shared Kernel)
- Share: CustomerId, CustomerName
- Separate: CreditLimit (Sales only), SupportTickets (Support only)
Inventory Context → Sales Context (Upstream/Downstream)
- Inventory publishes StockUpdated events
- Sales consumes events to update product availability
Tactical Design Patterns:
// Order is the Aggregate Root
class Order {
private items: OrderItem[] = [];
// Enforce business invariants
addItem(product: Product, quantity: number) {
if (quantity <= 0) throw new Error('Quantity must be positive');
// Business rule: max 10 items per order
if (this.items.length >= 10) throw new Error('Order cannot exceed 10 items');
this.items.push(new OrderItem(product, quantity));
}
// Aggregate ensures consistency
get total(): number {
return this.items.reduce((sum, item) => sum + item.subtotal, 0);
}
}
// OrderItem is NOT an Aggregate Root, only accessible through Order
class OrderItem {
constructor(public product: Product, public quantity: number) {}
get subtotal(): number {
return this.product.price * this.quantity;
}
}
class Money {
constructor(public readonly amount: number, public readonly currency: string) {
if (amount < 0) throw new Error('Amount cannot be negative');
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
// Immutable, defined by value (not identity)
const price1 = new Money(100, 'USD');
const price2 = new Money(100, 'USD');
console.log(price1.equals(price2)); // true (same value)
class OrderPlaced {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly total: Money,
public readonly occurredAt: Date
) {}
}
class Order {
private events: DomainEvent[] = [];
place() {
if (this.status !== 'draft') throw new Error('Order already placed');
this.status = 'placed';
this.events.push(new OrderPlaced(this.id, this.userId, this.total, new Date()));
}
getEvents(): DomainEvent[] {
return this.events;
}
}
Best For:
Team Context:
Technical Context:
Business Context:
| Architecture | Complexity | Scalability | Ops Overhead | Best For |
|---|---|---|---|---|
| Monolith | Low | Medium | Low | Startups, MVPs |
| Modular Monolith | Medium | Medium | Low | Growing startups |
| Microservices | High | High | High | Large orgs |
| Serverless | Low | Very High | Very Low | Event-driven, variable load |
| Event-Driven | High | Very High | High | High throughput, audit trails |
Monolith → Modular Monolith:
Modular Monolith → Microservices:
Monolith → Serverless:
Microservices with tight coupling - worst of both worlds.
❌ Symptom: Services can't deploy independently (require coordinated releases) ✅ Fix: Introduce message bus, versioned APIs, backward compatibility
Starting with microservices before understanding domain.
❌ Symptom: Constantly moving logic between services ✅ Fix: Start with modular monolith, extract services when boundaries are clear
Entities with only getters/setters, all logic in services.
❌ Symptom: Entities are just data containers ✅ Fix: Move business logic into domain entities
Single aggregate managing entire system state.
❌ Symptom: All commands touch the same aggregate ✅ Fix: Split into smaller aggregates with clear boundaries
Ask me about: