0. SOLID Principles (Fundamental Software Engineering Architecture)
0.1 Single Responsibility Principle (SRP)
Core Rule: "A class should have one, and only one, reason to change."
Deep Explanation & Architectural Reasoning:
SRP is about cohesion and responsibility actors. A class has a single responsibility if it serves only one actor (e.g., UI Team, Accounting Team, or DB Admin). When a single class handles UI rendering, HTTP network requests, data validation, and PDF generation, a change requested by Accounting risks breaking the UI or data layer unexpectedly.
InvoiceComponent handling API calls, tax math, local storage caching, and PDF generation.
// ❌ BAD: InvoiceComponent has 5 separate reasons to change!
@Component({ selector: 'app-invoice', template: `...` })
export class InvoiceComponent implements OnInit {
invoiceItems: any[] = [];
totalAmount = 0;
ngOnInit() {
// Reason 1: API Endpoint changes
fetch('/api/invoices/current').then(r => r.json()).then(data => {
this.invoiceItems = data.items;
// Reason 2: Tax formula changes
let sub = this.invoiceItems.reduce((sum, i) => sum + (i.price * i.qty), 0);
this.totalAmount = sub + (sub * 0.18);
// Reason 3: Caching requirement changes
localStorage.setItem('last_inv_total', this.totalAmount.toString());
});
}
// Reason 4: PDF formatting changes
downloadPDF() { const doc = new jsPDF(); doc.text(`Total: ${this.totalAmount}`, 10, 10); doc.save('inv.pdf'); }
}
🅰️ Angular Refactored SRP Services:
// 1. Data Layer
@Injectable({ providedIn: 'root' })
export class InvoiceApiService {
private api = inject(ApiInterfaceService);
getInvoice() { return this.api.get<Invoice>('invoices/current'); }
}
// 2. Business Logic Layer
@Injectable({ providedIn: 'root' })
export class TaxCalculatorService {
calculateTotalWithTax(items: InvoiceItem[], vatRate = 0.18): number {
const subtotal = items.reduce((s, i) => s + (i.price * i.qty), 0);
return subtotal + (subtotal * vatRate);
}
}
🔷 .NET Refactored SRP Architecture:
// 1. Repository Interface
public interface IInvoiceRepository {
Task<Invoice> GetByIdAsync(Guid id);
}
// 2. Domain Tax Calculator
public class TaxDomainService {
public decimal CalculateVat(decimal subtotal, decimal rate = 0.18m)
=> subtotal + (subtotal * rate);
}
// 3. PDF Generator Service
public class PdfInvoiceGenerator : IPdfInvoiceGenerator { ... }
0.2 Open/Closed Principle (OCP)
Core Rule: "Software entities should be open for extension, but closed for modification."
Deep Explanation & Architectural Reasoning:
Modifying existing source code introduces high regression risks — every edit forces re-testing all existing workflows across the system. By leveraging polymorphism, dependency injection, and abstraction contracts, new capabilities are added by creating new classes without editing existing working code.
switch/case or if/else block that requires modifying the class every time a new carrier or payment method is added.
// ❌ BAD: Adding a new carrier (Aramex) requires modifying existing source code!
export class ShippingCostCalculator {
calculateCost(carrier: string, weight: number): number {
switch (carrier) {
case 'DHL': return weight * 12.5 + 5.0;
case 'FedEx': return weight * 14.0 + 3.0;
case 'Aramex': // <-- Modifying existing code risks breaking DHL & FedEx!
return weight * 10.0 + 4.0;
default: throw new Error(`Unsupported carrier: ${carrier}`);
}
}
}
🅰️ Angular OCP Strategy Implementation:
export interface IShippingStrategy {
readonly carrierName: string;
calculateFee(weightKg: number): number;
}
@Injectable({ providedIn: 'root' })
export class DhlStrategy implements IShippingStrategy {
readonly carrierName = 'DHL';
calculateFee(w: number) { return w * 12.5 + 5.0; }
}
@Injectable({ providedIn: 'root' })
export class AramexStrategy implements IShippingStrategy {
readonly carrierName = 'Aramex'; // NEW class! Zero edits to DHL!
calculateFee(w: number) { return w * 10.0 + 4.0; }
}
🔷 .NET C# OCP Implementation:
public interface ICourierStrategy {
string ProviderName { get; }
decimal CalculateRate(decimal weight);
}
public class FedExService : ICourierStrategy {
public string ProviderName => "FedEx";
public decimal CalculateRate(decimal w) => w * 14.0m + 3.0m;
}
0.3 Liskov Substitution Principle (LSP)
Core Rule: "Subtypes must be substitutable for their base types without altering correctness or throwing unexpected errors."
Deep Explanation & Architectural Reasoning:
LSP guarantees that any subclass can be substituted wherever its parent class is expected without breaking caller code. Overriding a base method to throw NotImplementedException or throwing errors on valid inputs violates LSP.
// ❌ BAD: Subclass throws an error for base class method withdraw()
export class BankAccount {
protected balance = 0;
withdraw(amount: number) { this.balance -= amount; }
}
export class FixedDepositAccount extends BankAccount {
override withdraw(amount: number) {
// 💥 VIOLATION: Throws runtime error breaking code that expects BankAccount!
throw new Error("Withdrawals are locked on Fixed Deposit accounts!");
}
}
🅰️ Angular / TypeScript LSP Refactoring:
export abstract class BaseAccount {
protected balance = 0;
getBalance() { return this.balance; }
}
export abstract class TransactionalAccount extends BaseAccount {
abstract withdraw(amount: number): void;
}
export class CheckingAccount extends TransactionalAccount {
withdraw(amt: number) { this.balance -= amt; }
}
export class FixedDepositAccount extends BaseAccount {
// Does NOT inherit withdraw() -> Cannot break caller contracts!
}
🔷 .NET C# LSP Hierarchy:
public abstract class Bird { public abstract void Eat(); }
public interface IFlyingBird { void Fly(); }
public class Eagle : Bird, IFlyingBird {
public override void Eat() { ... }
public void Fly() { ... }
}
public class Ostrich : Bird {
public override void Eat() { ... } // No Fly() stub!
}
0.4 Interface Segregation Principle (ISP)
Core Rule: "Clients should not be forced to depend upon interfaces that they do not use."
Deep Explanation & Architectural Reasoning:
ISP prevents bloated "Fat Interfaces". Large monolithic interfaces force implementing classes to write dummy stubs or throw errors for methods they do not care about. Creating small, role-specific interfaces keeps components lean and decoupled.
// ❌ BAD: Fat Interface with 5 unrelated methods
export interface IEnterpriseDevice {
print(docId: string): void;
scan(): void;
fax(num: string): void;
ocrExtract(): string;
}
export class BasicPrinter implements IEnterpriseDevice {
print(id: string) { console.log("Printing..."); }
scan() { throw new Error("No scanner!"); } // Forced stub!
fax(n: string) { throw new Error("No fax!"); } // Forced stub!
ocrExtract() { throw new Error("No OCR!"); } // Forced stub!
}
🅰️ Angular / TypeScript ISP Interfaces:
export interface IPrinter { print(docId: string): void; }
export interface IScanner { scan(): Blob; }
export interface IFax { fax(num: string, doc: Blob): void; }
// Basic Desktop Printer depends ONLY on IPrinter
export class BasicPrinter implements IPrinter {
print(id: string) { console.log(`Printing: ${id}`); }
}
🔷 .NET C# Role Interfaces:
public interface IReadRepository<T> {
Task<T?> GetByIdAsync(Guid id);
}
public interface IWriteRepository<T> {
Task AddAsync(T entity);
Task DeleteAsync(Guid id);
}
// Read-only view model service implements ONLY IReadRepository
public class UserQueryService : IReadRepository<UserDto> { ... }
0.5 Dependency Inversion Principle (DIP)
Core Rule: "High-level modules should not depend on low-level modules. Both should depend on abstractions."
Deep Explanation & Architectural Reasoning:
DIP decouples core business logic from concrete technical infrastructure (SQL DBs, Axios, LocalStorage). High-level modules request interfaces via Dependency Injection, making the codebase 100% unit-testable with mock implementations.
new.
// ❌ BAD: OrderService tightly coupled to concrete Axios driver and SqlLogger
import { SqlLogger } from './sql-logger';
import { AxiosDriver } from './axios-driver';
export class OrderService {
private logger = new SqlLogger(); // Tight coupling!
private http = new AxiosDriver(); // Tight coupling!
placeOrder(order: any) {
this.logger.log(`Placing: ${order.id}`);
this.http.post('/api/orders', order);
}
}
🅰️ Angular DIP Implementation:
@Injectable({ providedIn: 'root' })
export class OrderService {
// Angular DI injects abstract ApiInterfaceService
private api = inject(ApiInterfaceService);
private notify = inject(NotificationService);
placeOrder(order: OrderPayload): Observable<OrderResult> {
return this.api.post<OrderResult>('orders', order);
}
}
🔷 .NET C# DIP Registration:
// Depend on IOrderRepository abstraction
public class OrderCommandHandler {
private readonly IOrderRepository _repository;
public OrderCommandHandler(IOrderRepository repo) => _repository = repo;
}
// In Program.cs: Register abstraction to concrete
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
1. Creational Design Patterns (Object Creation)
1.1 Singleton Pattern
🅰️ Angular Implementation:
@Injectable({ providedIn: 'root' })
export class AuthService {
private user = signal<User | null>(null);
}
🔷 .NET C# Implementation:
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
1.2 Factory Method / Abstract Factory Pattern
🅰️ Angular Notification Factory:
getProvider(type: 'email' | 'sms') {
return type === 'email' ? inject(EmailService) : inject(SmsService);
}
🔷 .NET Payment Gateway Factory:
public IPaymentProcessor Create(PaymentMethod m) => m switch {
PaymentMethod.Stripe => new StripeProcessor(),
PaymentMethod.PayPal => new PayPalProcessor()
};
1.3 Builder Pattern
🅰️ Angular FormBuilder:
this.form = this.fb.group({ title: [''], amount: [0] });
🔷 .NET WebApplicationBuilder:
var app = WebApplication.CreateBuilder(args).Build();
1.4 Prototype Pattern (Cloning)
🅰️ Angular / JS Implementation:
const clonedOffer = structuredClone(masterOfferTemplate);
🔷 .NET C# Implementation:
public class Invoice : ICloneable {
public object Clone() => this.MemberwiseClone();
}
2. Structural Design Patterns (Composition & Interfaces)
2.1 Adapter Pattern
🅰️ Angular Model Adapter:
adapt(dto: any): UserModel {
return new UserModel(dto.user_id, dto.first_name);
}
🔷 .NET Third-Party Adapter:
public class SoapEmailAdapter : IEmailSender { ... }
2.2 Decorator Pattern
🅰️ Angular Decorator:
@Component({ selector: 'app-card', ... })
🔷 .NET Caching Decorator:
public class CachedRepository : IRepository { ... }
2.3 Facade Pattern
🅰️ Angular State Facade:
export class OffersFacade {
readonly offers$ = this.store.select(selectOffers);
}
🔷 .NET Checkout Facade:
public async Task Checkout() {
await _payment.Charge(); await _stock.Deduct();
}
2.4 Composite Pattern (Tree Hierarchy)
🅰️ Angular Recursive Tree Component:
<app-tree-node *ngFor="let node of node.children" [node]="node" />
🔷 .NET Organizational Hierarchy:
public class Manager : IEmployee {
private List<IEmployee> _subordinates;
}
3. Behavioral Design Patterns (Algorithms & Communications)
3.1 Observer Pattern
🅰️ Angular RxJS / Signals:
this.userSubject$.subscribe(user => ...);
🔷 .NET MediatR Domain Events:
public class SendEmailOnOrderPlaced : INotificationHandler<OrderPlacedEvent>
3.2 Strategy Pattern
🅰️ Angular Sorting Strategy:
export class SortByPriceStrategy implements ISortStrategy { ... }
🔷 .NET Payment Strategy:
public class StripePaymentStrategy : IPaymentStrategy { ... }
3.3 Chain of Responsibility Pattern
🅰️ Angular HTTP Interceptors:
return next(authReq); // Passes to next interceptor
🔷 .NET Middleware Pipeline:
await _next(context); // Passes to next middleware
3.4 Mediator Pattern
🅰️ Angular Parent Component / Event Bus:
this.eventBus.emit('FILTER_CHANGED', filterValue);
🔷 .NET MediatR Command Dispatcher:
await _mediator.Send(new CreateOrderCommand(dto));
3.5 State Pattern (Finite State Machine)
🅰️ Angular Order Workflow FSM:
export class DraftState implements OrderState { ... }
🔷 .NET C# State Machine:
public class ShippedState : IOrderState { ... }
3.6 Template Method Pattern
🅰️ Angular Abstract Base Component:
export abstract class BaseGridComponent { ... }
🔷 .NET C# Abstract Base Service:
public abstract class DataProcessor { ... }
4. Senior & PM Level Enterprise System Architectures
4.1 Angular Facade Service Pattern (Decoupling Components from Direct Services)
1. Bad Practice (Direct Injection Spaghetti):
// ❌ Messy Component injecting 5 services directly:
constructor(
private offerService: OfferService,
private vendorService: VendorService,
private authService: AuthService,
private notifyService: NotificationService,
private router: Router
) {}
2. Senior Best Practice: The Facade Service Layer (`offers.facade.ts`):
@Injectable({ providedIn: 'root' })
export class OffersFacadeService {
// 1. Encapsulate all internal services & RxJS/Signal streams inside Facade:
private readonly offerService = inject(OfferService);
private readonly vendorService = inject(VendorService);
private readonly notifyService = inject(NotificationService);
// 2. Expose clean public reactive state to HTML components:
readonly offers = this.offerService.offersList;
readonly isLoading = this.offerService.loadingState;
// 3. Expose high-level user action methods:
loadOffersForCurrentVendor(): void {
this.vendorService.currentVendor$.pipe(
switchMap(vendor => this.offerService.getByVendor(vendor.id))
).subscribe();
}
}
3. Clean Component Template Usage (`offers.component.ts`):
@Component({ ... })
export class OffersComponent {
// ✅ Component injects ONLY 1 Facade Service! Super clean & 100% easy to test!
readonly facade = inject(OffersFacadeService);
ngOnInit() {
this.facade.loadOffersForCurrentVendor();
}
}
4.2 Clean Architecture / Onion Architecture (.NET C# Backend)
| Onion Layer | Project Name in .NET Solution | What Code Belongs Here? |
|---|---|---|
| 1. Domain Core (Center Seed) |
LP2.Domain.csproj |
Pure C# Entities, Value Objects, Domain Exceptions, Repository Interfaces. Zero external NuGet packages! |
| 2. Application Layer (Use Cases) |
LP2.Application.csproj |
CQRS Commands & Queries (MediatR), DTOs, FluentValidation rules, Application Interfaces. |
| 3. Infrastructure Layer (External Details) |
LP2.Infrastructure.csproj |
Entity Framework Core `DbContext`, SQL DB Migrations, SendGrid Email SDK, Redis Cache implementation. |
| 4. Presentation Layer (Outer Shell) |
LP2.WebApi.csproj |
ASP.NET Core Web API Controllers, Minimal API endpoints, Swagger UI, Middleware. |
4.3 Smart (Container) vs Presentational (Dumb) Component Pattern
| Component Type | Responsibilities | Services Injected? | Reusability & Testing |
|---|---|---|---|
Smart (Container) ComponentOffersContainerComponent |
Handles routing params, injects Facade/Services, subscribes to async state streams. Has ZERO complex HTML layout. | YES (Injects Facade/Services) | Specific to application feature route. |
Presentational (Dumb) ComponentOfferCardComponent |
Pure UI template. Receives data via @Input() offer and emits user actions via @Output() onSelect. |
NO! Zero injected services! ❌ | 100% Reusable anywhere! Easiest unit testing without mocking! ✅ |
5. Advanced Principal Architect System Design Patterns
5.1 Transactional Outbox Pattern (Reliable Event-Driven Messaging)
Problem & Principal Solution:
Problem: Saving an Order to SQL Database succeeds, but publishing the OrderPlaced event to RabbitMQ/Kafka fails due to a network glitch. Your database and message broker are now out of sync! ❌
Solution: Save both the Order Entity AND the Message Event into an OutboxMessages table inside the SAME atomic SQL Database Transaction. A background worker service (Hangfire / Quartz.NET) continuously polls the `OutboxMessages` table and publishes them to RabbitMQ reliably!
5.2 Strangler Fig Pattern (Legacy Monolith Migration)
Principal Architect Solution:
Never attempt a "Big Bang" rewrite of a 10-year-old legacy ASP.NET WebForms / AngularJS application—it will fail. Instead, place an API Gateway / Reverse Proxy (YARP / Nginx) in front of the legacy app. Migrate 1 module at a time to Angular 21 & .NET 9. Route modern paths (`/offers`) to the new app while legacy paths (`/billing`) continue running on the old app until 100% migrated!
5.3 Circuit Breaker & Retry Pattern (Polly & RxJS)
| Circuit State | System Behavior | Why it Saves the Server |
|---|---|---|
| 🟢 Closed (Normal) | All HTTP requests flow through normally. | Everything is healthy. |
| 🔴 Open (Tripped) | If 5 consecutive API calls fail, the Circuit Breaker trips! All new requests fail-fast immediately without hitting the dead external server. | Prevents 1,000 user requests from locking up server threads waiting 30 seconds for timeouts! ⚡ |
| 🟡 Half-Open (Trial) | Sends 1 trial request to see if external API recovered. If success ➡️ reset to Closed! | Automated self-healing recovery. |
6. Enterprise Architectural Patterns Summary
6.1 Repository, Unit of Work & CQRS Summary
| Pattern Name | Real-World Purpose | Angular / Frontend Role | .NET C# Backend Role |
|---|---|---|---|
| Repository Pattern | Decouples business logic from database/API data access details. | Service classes encapsulating HttpClient (`OffersService`). |
IRepository<T> wrapping EF Core DB contexts (`SqlRepository |
| Unit of Work Pattern | Guarantees multiple database repository changes are saved in 1 single atomic DB transaction. | Batch state dispatch in NgRx. | IUnitOfWork.SaveChangesAsync() executing EF Core atomic transactions. |
| CQRS Pattern | Separates Read Operations (Queries) from Write Operations (Commands) for high performance. | NgRx Actions (Commands) vs Selectors (Queries). | MediatR `IRequest |
Post a Comment