Software Architecture Mastery Program
5 Days | 50 Hours | From Foundations to Expert-Level System Design
How This Program Works
- 10 hours/day, 5 days — structured in 2-hour deep-dive blocks (5 blocks/day).
- Every pattern is analyzed against 6 quality attributes: Readability, Maintainability, Performance, Security, Scalability, Optimization.
- Every pattern includes: Problem → Solution → Real-World Example → Code → Trade-offs → When NOT to use.
- Each day ends with a hands-on lab and a self-check quiz (do these — passive reading won’t make you an expert).
- By Day 5, you design a full production-grade system from scratch (capstone).
Quality Attribute Legend used throughout:
| Symbol | Meaning |
|---|---|
| 📖 Readability | How easily a new engineer understands the code/structure |
| 🔧 Maintainability | Cost of changing/extending the system over time |
| ⚡ Performance | Latency, throughput, resource efficiency |
| 🔒 Security | Attack surface, data protection, trust boundaries |
| 📈 Scalability | Behavior under 10x, 100x, 1000x load |
| 🎯 Optimization | Specific tuning techniques applicable to the pattern |
DAY 1 — Foundations of Software Architecture (10 hrs)
Block 1 (Hrs 1–2): Architectural Thinking & Quality Attributes
1.1 What Architecture Actually Is
Architecture is the set of decisions that are expensive to change later — component boundaries, data flow, communication protocols, technology choices. Code structure is tactical; architecture is strategic.
The 4 Forces every architect balances:
- Business requirements (time-to-market, cost)
- Quality attributes (the 6 above)
- Team structure (Conway’s Law — your system will mirror your org chart)
- Technical constraints (legacy systems, compliance, existing infra)
1.2 Conway’s Law — Real Example
Amazon’s move to microservices in the early 2000s was driven by Bezos’s mandate that every team expose functionality only through APIs. This wasn’t just a technical decision — it restructured how ~10,000 engineers organized themselves into independent “two-pizza teams.” The architecture (microservices) and the org structure (autonomous teams) evolved together. This directly enabled AWS: internal APIs became sellable external products.
1.3 The Cost-of-Change Curve
A defect found at design time costs ~1x to fix. Found in production, it costs 100x–1000x (Boehm’s data, still broadly valid). This is why architecture reviews happen before coding — you’re front-loading expensive-to-reverse decisions.
1.4 Documenting Architecture Decisions (ADRs)
Every serious engineering org (Spotify, ThoughtWorks, AWS) uses Architecture Decision Records — lightweight docs capturing what was decided, why, and what alternatives were rejected.
# ADR-001: Use PostgreSQL over MongoDB for Order Service
Status: Accepted
Context: Order service needs ACID transactions across order+inventory+payment rows.
Decision: PostgreSQL, given strong consistency and mature transaction support.
Consequences: Requires explicit schema migrations (Flyway); horizontal scaling
needs read replicas or Citus later. MongoDB was rejected because eventual
consistency risks overselling limited-stock items.
📖🔧 Why this matters: ADRs are the single highest-leverage practice for maintainability. Six months later, nobody remembers why Postgres was chosen — the ADR does.
Block 2 (Hrs 3–4): Layered Architecture & MVC Family
2.1 Layered (N-Tier) Architecture
The oldest, most universally understood pattern. Strict layers: Presentation → Business Logic → Data Access → Database, each layer only calls the layer directly below it.
┌─────────────────────────┐
│ Presentation (UI/API) │
├─────────────────────────┤
│ Business Logic Layer │
├─────────────────────────┤
│ Data Access Layer │
├─────────────────────────┤
│ Database │
└─────────────────────────┘
Real-world example: Classic enterprise Java (Spring MVC) banking backends still use this today — Controller → Service → Repository → JPA/Hibernate → Oracle DB. Predictable, auditable, easy to onboard new engineers into — a top requirement in regulated banking environments.
// Presentation
@RestController
class AccountController {
private final AccountService service;
@GetMapping("/accounts/{id}")
ResponseEntity<AccountDTO> get(@PathVariable String id) {
return ResponseEntity.ok(service.getAccount(id));
}
}
// Business Logic
@Service
class AccountService {
private final AccountRepository repo;
AccountDTO getAccount(String id) {
Account acc = repo.findById(id).orElseThrow(NotFoundException::new);
return AccountMapper.toDTO(acc);
}
}
// Data Access
interface AccountRepository extends JpaRepository<Account, String> {}
Quality Attribute Analysis:
| Attribute | Assessment |
|---|---|
| 📖 Readability | Excellent — everyone knows where to look |
| 🔧 Maintainability | Good for small-medium apps; degrades as business logic layer bloats into a “God Service” |
| ⚡ Performance | Each layer adds a call hop — negligible in-process, but tempting to leak DB calls into controllers if undisciplined |
| 🔒 Security | Centralizing auth at presentation layer is easy; risk is business layer trusting all callers |
| 📈 Scalability | Scales vertically well; horizontal scaling requires the whole app to be stateless |
| 🎯 Optimization | Add caching layer between Business and Data layers; use DTOs to avoid over-fetching |
When NOT to use: Systems with many independent bounded contexts (use microservices), or UIs needing complex state synchronization (use MVVM/Flux instead).
2.2 MVC vs MVP vs MVVM — When Each Wins
| Pattern | Data flow | Best for | Real example |
|---|---|---|---|
| MVC | View→Controller→Model→View | Web apps, server-rendered | Ruby on Rails, Django, Spring MVC |
| MVP | View↔Presenter↔Model | Testable UI, Android (older) | Legacy Android apps pre-Jetpack |
| MVVM | View↔ViewModel (data-binding)↔Model | Rich client apps with reactive UI | iOS/Android with SwiftUI/Jetpack Compose, Angular, WPF |
Real-world deep dive — MVVM in a banking app:
Jetpack Compose banking apps (e.g., Google Pay style apps) use MVVM because balance updates need to reactively flow to UI without the View manually pulling data.
class AccountViewModel(private val repo: AccountRepository) : ViewModel() {
private val _balance = MutableStateFlow(0.0)
val balance: StateFlow<Double> = _balance.asStateFlow()
fun loadBalance(accountId: String) = viewModelScope.launch {
_balance.value = repo.fetchBalance(accountId) // auto-updates UI via StateFlow
}
}
📖🔧 Why it matters: The ViewModel has zero Android/UI framework imports — 100% unit-testable, and survives configuration changes (screen rotation) without re-fetching data. This single decision cut crash-related re-fetch bugs significantly in production Android codebases.
Block 3 (Hrs 5–6): Clean / Hexagonal / Onion Architecture
3.1 The Core Idea: Dependency Inversion at the Architecture Level
Business logic should not depend on frameworks, databases, or UI. Dependencies point inward, toward the domain.
┌───────────────────────────┐
│ Frameworks & Drivers │ (DB, Web, UI, external APIs)
│ ┌─────────────────────┐ │
│ │ Interface Adapters │ │ (Controllers, Gateways, Presenters)
│ │ ┌───────────────┐ │ │
│ │ │ Use Cases │ │ │ (Application business rules)
│ │ │ ┌───────────┐ │ │ │
│ │ │ │ Entities │ │ │ │ (Enterprise business rules — the core)
│ │ │ └───────────┘ │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────┘ │
└───────────────────────────┘
Dependencies point INWARD only
3.2 Real-World Example: Netflix’s Edge Services
Netflix’s internal services use Hexagonal Architecture (Ports & Adapters) so the same core recommendation logic can be tested with in-memory fake repositories in CI, then swapped to real Cassandra/EVCache adapters in production — without touching a single line of business logic. This cut integration-test flakiness dramatically because 90% of tests run against fast in-memory adapters.
// PORT (interface, lives in the domain — no framework dependency)
interface PaymentGateway {
PaymentResult charge(Money amount, CardToken token);
}
// DOMAIN USE CASE (pure business logic, zero framework imports)
class ProcessOrderUseCase {
private final PaymentGateway paymentGateway; // depends on abstraction, not Stripe/PayPal
private final OrderRepository orderRepository;
OrderResult execute(OrderRequest request) {
Order order = Order.create(request);
PaymentResult result = paymentGateway.charge(order.getTotal(), request.getCardToken());
if (!result.isSuccessful()) throw new PaymentFailedException();
order.markPaid();
orderRepository.save(order);
return OrderResult.success(order);
}
}
// ADAPTER (framework-specific, swappable)
class StripePaymentAdapter implements PaymentGateway {
public PaymentResult charge(Money amount, CardToken token) {
// Stripe SDK calls here
}
}
Quality Attribute Analysis:
| Attribute | Assessment |
|---|---|
| 📖 Readability | Steeper learning curve initially; extremely clear once understood |
| 🔧 Maintainability | Best-in-class — swap Stripe for PayPal by writing one new adapter, zero domain changes |
| ⚡ Performance | Slight indirection overhead (interfaces), negligible at runtime with modern JIT/JVM |
| 🔒 Security | Domain logic is isolated from I/O — reduces risk of injection bugs leaking into business rules |
| 📈 Scalability | Excellent — use cases are stateless and easy to parallelize/test independently |
| 🎯 Optimization | Enables parallel adapter development across teams; mock adapters make load-testing use cases in isolation trivial |
When NOT to use: Small CRUD apps / prototypes — the ceremony (ports, adapters, mappers) is overkill if the app will never need to swap infrastructure.
Block 4 (Hrs 7–8): SOLID, GRASP & DDD as Architectural Drivers
4.1 SOLID at the Architecture Level (not just class level)
- Single Responsibility → maps to bounded contexts / microservice boundaries, not just classes
- Open/Closed → plugin architectures (e.g., VS Code extensions, Jenkins plugins)
- Liskov Substitution → why hexagonal adapters must be truly interchangeable
- Interface Segregation → why API gateways expose narrow, purpose-specific endpoints instead of one giant API
- Dependency Inversion → the entire basis of Clean/Hexagonal Architecture above
4.2 Domain-Driven Design (DDD) Essentials
Real-world example: Uber’s backend is organized around DDD Bounded Contexts: Trip, Pricing, Driver Matching, Payments are separate contexts with their own ubiquitous language — a “Trip” in the Pricing context means something subtly different (a priceable fare unit) than in the Driver-Matching context (a dispatch assignment). Each context maps to independently deployable services.
Key DDD building blocks:
| Concept | Definition | Example |
|---|---|---|
| Entity | Object with identity that persists over time | Order (same order, different states) |
| Value Object | Immutable, defined by its attributes | Money(amount, currency) |
| Aggregate | Cluster of entities with one root controlling consistency | Order (root) + OrderLineItems |
| Bounded Context | Explicit boundary where a model applies | Billing context vs Shipping context |
| Anti-Corruption Layer | Translation layer between two bounded contexts / legacy systems | Adapter translating legacy SOAP mainframe data into modern domain objects |
// Aggregate Root enforcing invariants — this is where business rules live
class Order {
private OrderId id;
private List<LineItem> items;
private OrderStatus status;
void addItem(Product product, int qty) {
if (status != OrderStatus.DRAFT)
throw new IllegalStateException("Cannot modify a submitted order");
items.add(new LineItem(product, qty));
}
Money total() {
return items.stream().map(LineItem::subtotal).reduce(Money.ZERO, Money::add);
}
}
📖🔧 Why aggregates matter: They are the transactional consistency boundary. Never let a transaction span two aggregates — this is one of the top causes of distributed-system deadlocks and inconsistency when teams later split aggregates into separate microservices without realizing the coupling.
Block 5 (Hrs 9–10): Day 1 Lab + Quiz
Hands-On Lab (2 hrs)
Take a simple “TODO app” with all logic in one Express.js/Spring controller file and refactor it into:
- Layered architecture (Controller/Service/Repository)
- Then refactor further into Hexagonal (define a
TaskRepositoryport, an in-memory adapter for tests, a real DB adapter for production) - Write an ADR explaining your choice of storage technology
Deliverable: A GitHub repo with both versions + the ADR. This is the single best exercise for internalizing why hexagonal architecture exists — you’ll feel the pain layered architecture doesn’t solve.
Day 1 Self-Check Quiz
- Why does Conway’s Law matter when choosing between monolith and microservices?
- In Clean Architecture, which direction do dependencies point, and why?
- Give an example of an Aggregate and explain why it defines a transaction boundary.
- What’s the difference between MVP and MVVM in terms of testability?
- Name 2 quality attributes that Hexagonal Architecture improves and 1 it can slightly hurt.
DAY 2 — Microservices & Distributed Communication Patterns (10 hrs)
Block 1 (Hrs 1–2): Monolith vs Microservices — The Real Decision Framework
1.1 It’s Not “Microservices Are Better” — It’s a Trade-off
Real-world cautionary tale: Segment.com famously moved to microservices (~150 services), then in 2018 wrote a public postmortem on moving back to a monolith for their core pipeline — the operational overhead (140+ repos, cross-service debugging) outweighed the benefits at their scale. Meanwhile, Amazon/Netflix/Uber thrive on microservices because they have hundreds of independent teams needing independent deploy cadence.
Decision framework:
| Choose Monolith when | Choose Microservices when |
|---|---|
| Team < 15-20 engineers | Multiple independent teams (Conway’s Law) |
| Domain not yet well understood | Bounded contexts are stable/well-understood |
| Need fast prototyping | Need independent scaling of components |
| Strong consistency needed everywhere | Can tolerate eventual consistency in places |
| Ops maturity is low | Have investment in CI/CD, observability, container orchestration |
🎯 Optimization tip: Start with a “modular monolith” — strict internal module boundaries (like Hexagonal bounded modules) inside one deployable. Shopify famously scaled to billions in GMV on a modular Rails monolith before selectively extracting only the highest-scale components (e.g., Checkout) into services.
Block 2 (Hrs 3–4): API Gateway, BFF & Service Discovery
2.1 API Gateway Pattern
Single entry point that handles routing, auth, rate limiting, and protocol translation so clients don’t need to know about internal service topology.
Client → API Gateway → [Auth check, rate limit, routing]
├──→ Order Service
├──→ Inventory Service
└──→ Pricing Service
Real-world example: Netflix’s Zuul (now largely replaced by Spring Cloud Gateway internally) handles ~1 billion+ requests/day at the edge, doing dynamic routing, canary deployment traffic-splitting, and DDoS protection before requests ever reach backend microservices.
# Example Spring Cloud Gateway route config
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
- name: CircuitBreaker
args:
name: orderServiceCB
fallbackUri: forward:/fallback/orders
Quality Attribute Analysis:
| Attribute | Assessment |
|---|---|
| 📖 Readability | Clients see one clean API surface instead of N services |
| 🔧 Maintainability | Centralizes cross-cutting concerns (auth, logging) — change once, not N times |
| ⚡ Performance | Adds one network hop; mitigate with connection pooling + gateway co-location |
| 🔒 Security | Critical security chokepoint — single place to enforce TLS termination, WAF rules, auth tokens |
| 📈 Scalability | Gateway itself must be horizontally scaled and stateless (a bottleneck if not) |
| 🎯 Optimization | Response caching at gateway layer for read-heavy, rarely-changing endpoints |
When NOT to use as-is: A single monolithic gateway serving wildly different client types (mobile vs web vs partner APIs) becomes a bottleneck for change → leads to the next pattern.
2.2 Backend for Frontend (BFF)
Instead of one gateway serving all clients, each client type gets its own tailored backend.
Real-world example: SoundCloud runs separate BFFs for iOS, Android, and Web — the mobile BFF aggregates and trims payloads aggressively (bandwidth-constrained), while the Web BFF can return richer, larger payloads. This avoids the classic “gateway team becomes a bottleneck for every client-specific change” problem.
2.3 Service Discovery
In dynamic environments (auto-scaling, container orchestration), service IPs constantly change. Services register themselves; consumers query a registry.
Real-world example: Netflix’s Eureka and Kubernetes’ built-in DNS-based service discovery both solve this — in K8s, a Service object gives a stable DNS name (order-service.default.svc.cluster.local) regardless of how many pod IPs churn underneath.
// Client-side discovery with Eureka + Ribbon/Spring Cloud LoadBalancer
@LoadBalanced
@Bean
RestTemplate restTemplate() { return new RestTemplate(); }
// Call by logical service name — resolved dynamically
restTemplate.getForObject("http://ORDER-SERVICE/orders/123", Order.class);
Block 3 (Hrs 5–6): Resilience Patterns — Circuit Breaker, Retry, Timeout, Bulkhead
3.1 Circuit Breaker Pattern
Prevents cascading failures by “tripping” after repeated failures, failing fast instead of piling up timeouts.
CLOSED (normal) --[failures exceed threshold]--> OPEN (fail fast)
^ |
| [after timeout, try 1 request]
| v
+---------[success]----------------------- HALF-OPEN
Real-world example: Netflix’s Hystrix (and its successor resilience4j) was born directly from a 2011 Netflix outage where one slow dependency (a recommendations service) exhausted thread pools across the entire fleet, cascading into a full site outage. Circuit breakers now wrap every inter-service call at Netflix.
@CircuitBreaker(name = "inventoryService", fallbackMethod = "fallbackInventory")
@Retry(name = "inventoryService")
@TimeLimiter(name = "inventoryService")
public CompletableFuture<Stock> checkStock(String sku) {
return CompletableFuture.supplyAsync(() -> inventoryClient.getStock(sku));
}
public CompletableFuture<Stock> fallbackInventory(String sku, Throwable t) {
return CompletableFuture.completedFuture(Stock.unknown(sku)); // graceful degradation
}
3.2 Bulkhead Pattern
Isolate resources (thread pools, connection pools) per dependency so one slow/failing dependency can’t starve resources needed by others — named after ship compartments that stop one leak from sinking the whole vessel.
🔒📈 Why it matters for security AND scalability: A bulkhead also limits blast radius of a compromised/abused dependency (e.g., a third-party API being slow due to attack) from taking down your entire service fleet.
3.3 Retry + Exponential Backoff + Jitter
Naive retries amplify outages (thundering herd). AWS’s own architecture blog explicitly recommends exponential backoff with jitter — without jitter, all clients retry in lockstep, re-creating the exact spike that caused the failure.
import random, time
def retry_with_backoff(fn, max_retries=5, base_delay=0.5):
for attempt in range(max_retries):
try:
return fn()
except TransientError:
delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5) # jitter
time.sleep(delay)
raise MaxRetriesExceeded()
Block 4 (Hrs 7–8): Service Mesh, Sidecar Pattern & Strangler Fig
4.1 Sidecar Pattern & Service Mesh
Deploy a proxy container alongside each service instance to handle cross-cutting concerns (mTLS, retries, observability) without embedding that logic in every service’s code.
Real-world example: Istio (built on Envoy proxy) is used at Airbnb, Salesforce, and eBay to enforce mTLS between every service-to-service call cluster-wide, without a single line of application code change — a huge win when you have 100s of services in different languages (Java, Go, Python) that would otherwise each need their own TLS/retry library.
┌─────────────────────┐
│ Pod │
│ ┌────────┐ ┌──────┐ │
│ │ App │↔│Envoy │←┼──── mTLS, retries, metrics, tracing
│ │Container│ │Sidecar│ │ all handled here, transparently
│ └────────┘ └──────┘ │
└─────────────────────┘
Quality Attribute Analysis:
| Attribute | Assessment |
|---|---|
| 🔒 Security | Massive win — uniform mTLS + policy enforcement without per-service code |
| ⚡ Performance | Adds ~1-2ms latency per hop (proxy overhead) — usually acceptable, must be measured |
| 🔧 Maintainability | Decouples infra concerns from app code — upgrade retry logic cluster-wide via config, not redeploys |
| 📈 Scalability | Essential at 50+ service scale; overkill below ~10-15 services |
4.2 Strangler Fig Pattern — Migrating Legacy Systems Safely
Named after the strangler fig vine that gradually envelops and replaces a host tree. Route traffic incrementally from a legacy monolith to new services, feature by feature, until the monolith can be decommissioned.
Real-world example: This is exactly how Amazon migrated its monolithic retail platform to services over years (2000s), and how most large enterprises (banks migrating COBOL mainframes) do it today — an API Gateway routes specific endpoints to the new service while everything else still goes to the old monolith, until nothing points at the old system anymore.
Phase 1: [Gateway] → 100% Monolith
Phase 2: [Gateway] → /checkout → New Service
→ everything else → Monolith
Phase 3: [Gateway] → /checkout, /cart → New Services
→ everything else → Monolith
Phase N: [Gateway] → 100% New Services → Monolith decommissioned
🔧 Why this matters: Big-bang rewrites fail more often than they succeed (see: numerous public rewrite failure post-mortems). Strangler Fig lets you de-risk by shipping incrementally and rolling back a single route if something breaks, instead of an all-or-nothing cutover.
Block 5 (Hrs 9–10): Day 2 Lab + Quiz
Hands-On Lab (2 hrs)
- Take 2 services (e.g.,
OrderService,InventoryService) and put an API Gateway (Spring Cloud Gateway, Kong, or NGINX) in front. - Add a circuit breaker (resilience4j) on the Order→Inventory call. Kill the Inventory service and observe fallback behavior.
- Add exponential backoff + jitter to a retry policy and log timestamps to visually confirm the jitter effect.
Day 2 Self-Check Quiz
- Why did Segment revert from microservices to a monolith, and what does that tell you about when NOT to use microservices?
- Draw the 3 states of a circuit breaker and explain the transition conditions.
- Why is jitter necessary in retry logic — what specific failure mode does it prevent?
- What problem does BFF solve that a single API Gateway doesn’t?
- Explain how Strangler Fig reduces risk compared to a full rewrite.
DAY 3 — Data, Messaging & Event-Driven Architecture Patterns (10 hrs)
Block 1 (Hrs 1–2): Database-per-Service & Data Consistency Challenges
1.1 Database-per-Service Pattern
Each microservice owns its data exclusively; no other service may query its DB directly. This enforces true bounded-context isolation, but breaks the easy joins/transactions you had in a monolith.
OrderService ──owns──> orders_db (PostgreSQL)
InventoryService ──owns──> inventory_db (PostgreSQL)
RecommendationService ──owns──> recs_db (Cassandra) ← different DB tech is OK!
Real-world example: Uber’s services use polyglot persistence — Schemaless (MySQL-backed) for trip data needing durability, Cassandra for time-series telemetry from driver apps, and Redis for real-time ETA caches. Each choice matches the access pattern, not a company-wide “one database to rule them all” mandate.
🔧 The hard trade-off: You lose cross-service JOINs and multi-service ACID transactions. This is the single biggest mental shift for engineers moving from monolith to microservices, and it’s why the next 3 patterns (Saga, CQRS, Event Sourcing) exist.
Block 2 (Hrs 3–4): Saga Pattern — Distributed Transactions
2.1 The Problem
An order requires: reserve inventory → charge payment → schedule shipping. In a monolith, this is one DB transaction. Across 3 services, you can’t use a single ACID transaction (2PC/XA exists but is notoriously fragile and doesn’t scale well — most companies avoid it).
2.2 Saga Pattern: Choreography vs Orchestration
Choreography (event-driven, no central coordinator):
OrderService: creates order → publishes OrderCreated
InventoryService: listens OrderCreated → reserves stock → publishes StockReserved
PaymentService: listens StockReserved → charges card → publishes PaymentCompleted
ShippingService: listens PaymentCompleted → schedules shipment
(if PaymentFailed → InventoryService listens & releases stock — COMPENSATION)
Orchestration (central saga coordinator):
class OrderSagaOrchestrator {
void handle(OrderCreatedEvent event) {
try {
inventoryClient.reserve(event.getOrderId(), event.getItems());
paymentClient.charge(event.getOrderId(), event.getTotal());
shippingClient.schedule(event.getOrderId());
} catch (InventoryException e) {
// no compensation needed yet, nothing succeeded
} catch (PaymentException e) {
inventoryClient.release(event.getOrderId()); // COMPENSATING ACTION
}
}
}
Real-world example: Uber Eats uses Saga orchestration for order fulfillment — if a restaurant rejects an order after payment authorization, a compensating transaction refunds the customer and notifies the driver-matching service to cancel dispatch. This is documented in Uber’s engineering blog on their Cadence workflow orchestration engine (open-sourced, now also known via its successor Temporal), built specifically to make long-running Sagas reliable and resumable across failures.
Choreography vs Orchestration trade-offs:
| Choreography | Orchestration | |
|---|---|---|
| 📖 Readability | Harder — logic spread across N services | Easier — one place to read the whole flow |
| 🔧 Maintainability | Adding a step means touching multiple services | Add a step in one orchestrator |
| 📈 Scalability | No single point of coordination bottleneck | Orchestrator must scale/be made resilient |
| Best for | Few steps, simple flows | Complex, long-running, multi-step business processes |
Block 3 (Hrs 5–6): CQRS & Event Sourcing
3.1 CQRS (Command Query Responsibility Segregation)
Separate the write model (commands, enforces business rules) from the read model (queries, optimized for display) — they can even use different databases.
Write side: Command → OrderAggregate (validates rules) → Postgres (source of truth)
│
▼ (publishes OrderPlaced event)
Read side: Event → Projection builder → Elasticsearch (denormalized, fast search)
→ Redis (fast dashboard reads)
Real-world example: Amazon’s product catalog uses this exact split — writes to product data go through validated services, but the read path that renders product pages is served from a heavily denormalized, cached read-store (built for sub-100ms reads at massive scale) that’s eventually consistent with the write side by design (you’ve likely seen a “price updated a few seconds ago” lag — that’s CQRS at work).
Quality Attribute Analysis:
| Attribute | Assessment |
|---|---|
| ⚡ Performance | Read models tuned exactly for query patterns (denormalized, indexed, cached) — huge win |
| 📈 Scalability | Read and write sides scale independently (reads are usually 10-100x more frequent) |
| 🔧 Maintainability | More moving parts (2 models, sync mechanism) — real complexity cost |
| 📖 Readability | Can confuse newcomers (“why is data in 2 places?”) — needs strong documentation |
When NOT to use: Simple CRUD apps with low read/write ratio disparity — CQRS adds real complexity that only pays off at scale or with complex query needs.
3.2 Event Sourcing
Instead of storing current state, store the sequence of events that led to that state. Current state = replaying all events.
// Instead of: UPDATE accounts SET balance = 150 WHERE id = 1
// You store the events:
[
AccountOpened(id=1, balance=0),
MoneyDeposited(id=1, amount=200),
MoneyWithdrawn(id=1, amount=50)
]
// Current balance = replay events = 0 + 200 - 50 = 150
Real-world example: Banking and fintech systems (e.g., many core-banking platforms built on Axon Framework or EventStoreDB) use event sourcing because regulators require a full, immutable audit trail of every state change — “what was the balance at 3pm on March 5th” is trivial to answer (replay events up to that timestamp) but nearly impossible to reconstruct from a “current state only” database.
Quality Attribute Analysis:
| Attribute | Assessment |
|---|---|
| 🔒 Security | Full audit trail by construction — huge compliance win (SOX, PCI-DSS) |
| 🔧 Maintainability | Bugs are debuggable by replaying real event history; but schema evolution of events is hard |
| ⚡ Performance | Replaying long event streams is slow — mitigated with snapshots (periodic materialized state) |
| 📈 Scalability | Event store (e.g., Kafka, EventStoreDB) scales append-only writes very well |
🎯 Optimization — Snapshotting: Don’t replay 100,000 events every time. Persist a snapshot every N events (e.g., every 100), then replay only events after the snapshot.
Block 4 (Hrs 7–8): Messaging Patterns & Caching Strategies
4.1 Message Queue vs Pub/Sub
| Message Queue (e.g., SQS, RabbitMQ) | Pub/Sub (e.g., Kafka, SNS) | |
|---|---|---|
| Delivery | One consumer processes each message | Many subscribers get a copy of each message |
| Use case | Task distribution (work queue) | Event broadcasting (many services react) |
| Example | Image resize job queue | “OrderPlaced” event fanned out to Inventory, Analytics, Email services |
Real-world example: LinkedIn built Kafka specifically because they needed a durable, replayable, high-throughput pub/sub log — it now handles trillions of messages per day across LinkedIn’s infrastructure, and became the backbone of event-driven architecture industry-wide (used at Uber, Netflix, Airbnb, Spotify).
# Kafka producer - publishing a domain event
producer.send('order-events', key=order_id, value={
"eventType": "OrderPlaced",
"orderId": order_id,
"timestamp": time.time(),
"items": items
})
# Multiple independent consumer groups each get their own copy:
# consumer group "inventory-service" -> reserves stock
# consumer group "analytics-service" -> updates dashboards
# consumer group "email-service" -> sends confirmation
4.2 Caching Patterns
Cache-Aside (Lazy Loading) — most common:
def get_product(product_id):
cached = redis.get(f"product:{product_id}")
if cached: return cached # cache hit
product = db.query(product_id) # cache miss
redis.set(f"product:{product_id}", product, ex=300) # TTL 5 min
return product
Write-Through: Write to cache and DB simultaneously — reads are always fresh, but writes are slower.
Write-Behind (Write-Back): Write to cache immediately, asynchronously flush to DB later — fast writes, risk of data loss if cache fails before flush.
Real-world example: Twitter’s timeline uses a hybrid: a fan-out-on-write cache (pre-computing each user’s home timeline into Redis when someone they follow tweets) for most users, but fan-out-on-read (computed on demand) for celebrity accounts with millions of followers — otherwise one Beyoncé tweet would trigger tens of millions of cache writes instantly (the “celebrity problem”).
Quality Attribute Analysis (Caching in general):
| Attribute | Assessment |
|---|---|
| ⚡ Performance | Often the single highest-leverage optimization — orders of magnitude latency reduction |
| 🔒 Security | Caching sensitive data (PII, tokens) requires careful TTL + encryption-at-rest in cache |
| 📈 Scalability | Reduces DB load, letting the DB handle more effective throughput |
| 🎯 Optimization | Use cache stampede protection (locks/jitreadTTL) to prevent thundering herd on expiry |
🎯 Cache stampede protection example:
def get_product_safe(product_id):
cached = redis.get(f"product:{product_id}")
if cached: return cached
lock = redis.set(f"lock:product:{product_id}", "1", nx=True, ex=10)
if lock:
product = db.query(product_id)
redis.set(f"product:{product_id}", product, ex=300)
redis.delete(f"lock:product:{product_id}")
return product
else:
time.sleep(0.05)
return get_product_safe(product_id) # retry, someone else is refreshing
Block 5 (Hrs 9–10): Day 3 Lab + Quiz
Hands-On Lab (2 hrs)
- Implement a simple Saga (orchestrated) across 3 mock services (Order, Inventory, Payment) with a deliberate payment failure that triggers a compensating action.
- Set up Kafka locally (Docker), publish an
OrderPlacedevent, and have 2 separate consumer groups process it independently. - Implement cache-aside with stampede protection for a “product details” endpoint; load-test with/without the lock to see the difference in DB query count.
Day 3 Self-Check Quiz
- Why can’t you use a single ACID transaction across 3 microservices, and what pattern replaces it?
- What’s the key difference between Choreography and Orchestration Sagas, and when would you pick each?
- In CQRS, why might the read model be “eventually consistent,” and is that always acceptable?
- Why does Event Sourcing need snapshots, and what problem would you have without them?
- Explain the “celebrity problem” in fan-out caching and how Twitter’s hybrid approach solves it.
DAY 4 — Security Architecture & Performance Optimization Patterns (10 hrs)
Block 1 (Hrs 1–2): Zero Trust & Defense in Depth
1.1 Zero Trust Architecture
Traditional model: strong perimeter (firewall), trust everything inside. Zero Trust: “never trust, always verify” — every request, internal or external, is authenticated and authorized, regardless of network location.
Real-world example: Google’s internal BeyondCorp initiative (post-2010 Aurora attack, where perimeter trust was exploited) eliminated the internal “trusted network” concept entirely — every Google employee accesses internal apps the same way whether on a corporate network or a coffee shop Wi-Fi, authenticated per-request via device certs + user identity, not network location. This became the industry blueprint for Zero Trust (now codified in NIST SP 800-207).
Core Zero Trust principles applied architecturally:
- Verify explicitly (every request authenticated — mTLS, short-lived tokens)
- Least-privilege access (fine-grained, per-resource, not network-wide)
- Assume breach (segment, encrypt, log everything as if an attacker is already inside)
1.2 Defense in Depth — Layered Security
No single control is trusted alone; layer WAF → API Gateway auth → service-level authz → data encryption → audit logging.
Internet → [WAF: blocks OWASP Top 10 patterns]
→ [API Gateway: OAuth2 token validation, rate limiting]
→ [Service mesh: mTLS between services]
→ [Service: fine-grained authorization (RBAC/ABAC)]
→ [Database: encryption at rest, row-level security]
→ [Audit log: immutable, append-only, shipped off-box]
🔒 Why layering matters: In 2017, Equifax was breached via a single unpatched Apache Struts vulnerability with no additional layers stopping lateral movement once inside — 147 million records exposed. Defense in depth means one compromised layer doesn’t compromise the whole system.
Block 2 (Hrs 3–4): AuthN/AuthZ Patterns — OAuth2, OIDC, JWT
2.1 OAuth2 + OIDC in Practice
OAuth2 = authorization (what can you do). OIDC (built on OAuth2) = authentication (who are you). Almost every “Sign in with Google/GitHub” flow uses this.
1. User clicks "Login with Google"
2. Redirected to Google's authorization server
3. User consents; Google redirects back with an authorization code
4. Your backend exchanges the code for an access_token + id_token (OIDC)
5. id_token (a JWT) proves identity; access_token authorizes API calls
2.2 JWT Structure & Common Pitfalls
header.payload.signature
eyJhbGc... . eyJzdWI... . SflKxwRJ...
Real security pitfalls to know (asked in every senior architect interview):
| Pitfall | Why it’s dangerous | Fix |
|---|---|---|
Accepting alg: none |
Attacker forges unsigned token, server accepts it | Explicitly whitelist allowed algorithms server-side |
| Storing JWT in localStorage | Vulnerable to XSS token theft | Use httpOnly, Secure, SameSite=Strict cookies |
| Long-lived access tokens | Stolen token usable for a long window | Short-lived access token (5-15 min) + refresh token rotation |
| No signature verification on every request | Trusting payload blindly | Always cryptographically verify signature server-side, every request |
// Correct: explicit algorithm whitelist + short expiry
JwtParser parser = Jwts.parserBuilder()
.setSigningKey(publicKey)
.build();
// library configured to reject 'none' and only accept RS256
Claims claims = parser.parseClaimsJws(token).getBody();
if (claims.getExpiration().before(new Date())) throw new TokenExpiredException();
2.3 mTLS for Service-to-Service Auth
Every internal service call presents a certificate; both sides verify each other (mutual TLS) — this is what Istio/Envoy sidecars automate at scale (from Day 2).
Real-world example: Netflix requires mTLS between every microservice via their internal service mesh — even inside their “trusted” VPC, because Zero Trust assumes internal network compromise is possible too.
Block 3 (Hrs 5–6): API & Data Security Patterns
3.1 Secrets Management
Never hardcode credentials. Use a secrets manager with rotation.
Real-world example: HashiCorp Vault (used at Adobe, Bloomberg) issues dynamic, short-lived database credentials — instead of a static DB password embedded in config, a service requests a credential valid for 1 hour, and Vault automatically revokes it after. A leaked credential in logs/git history becomes useless after expiry instead of a permanent backdoor.
3.2 Rate Limiting & DDoS Protection Patterns
Token Bucket algorithm (most common, used by AWS API Gateway, Stripe API):
class TokenBucket:
def __init__(self, capacity, refill_rate):
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate # tokens per second
self.last_refill = time.time()
def allow_request(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
Real-world example: Stripe’s public API famously returns 429 Too Many Requests with a Retry-After header using exactly this pattern — a documented, predictable rate-limit contract is itself a security AND developer-experience feature.
3.3 Input Validation & the OWASP Top 10 at the Architecture Level
Architecturally, validation should happen at the boundary (API Gateway / controller layer) using strict schemas (e.g., JSON Schema, protobuf), not scattered if checks deep in business logic. This is a direct application of the Hexagonal Architecture principle from Day 1 — adapters sanitize input before it ever reaches the domain core.
🔒 SQL Injection — why parameterized queries are architectural, not just a “coding tip”:
// NEVER — string concatenation
String query = "SELECT * FROM users WHERE email = '" + email + "'";
// ALWAYS — parameterized, enforced at the Data Access Layer / Repository
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE email = ?");
stmt.setString(1, email);
The reason this is “architecture” and not just “coding style”: if every team’s Data Access Layer/ORM enforces parameterization by construction (e.g., using JPA/Hibernate/an ORM exclusively, banning raw SQL string-building), SQL injection becomes structurally impossible rather than dependent on every engineer remembering a rule.
Block 4 (Hrs 7–8): Performance & Optimization Patterns
4.1 The Performance Hierarchy (fix in this order)
- Algorithmic complexity (O(n²) → O(n log n)) — biggest wins, free of infra cost
- Database (indexing, N+1 queries, connection pooling)
- Caching (Day 3)
- Concurrency/Async (non-blocking I/O)
- Network (CDN, compression, connection reuse)
- Horizontal scaling (Day 5) — the “throw more hardware at it” last resort
4.2 The N+1 Query Problem (the #1 real-world performance killer)
# BAD: N+1 queries — 1 query for orders, then N queries for each order's items
orders = Order.objects.all()
for order in orders:
print(order.items.all()) # separate DB hit per order!
# GOOD: eager loading — 1 query total (or 2 with a JOIN)
orders = Order.objects.prefetch_related('items').all()
Real-world impact: This single pattern recognition (N+1) is one of the most common root causes of “why is our API slow” incidents at almost every company using an ORM (Rails ActiveRecord, Django ORM, Hibernate) — often turning a 50ms page load into 5000ms with just 100 records.
4.3 Connection Pooling
Opening a DB connection is expensive (TCP handshake, auth, TLS negotiation — often 5-50ms). Pools (HikariCP for Java, PgBouncer for Postgres) keep warm connections ready.
# HikariCP config — sizing matters more than people think
spring:
datasource:
hikari:
maximum-pool-size: 10 # NOT "as high as possible" — see formula below
minimum-idle: 5
connection-timeout: 3000
🎯 Optimization formula (from HikariCP’s own docs, based on PostgreSQL’s own recommendation):
pool_size = ((core_count * 2) + effective_spindle_count) — oversized pools cause more contention on the DB, not less, because of context-switching overhead.
4.4 Async/Non-Blocking I/O
Real-world example: Netflix rewrote parts of their API layer using RxJava reactive streams specifically because their API servers were spending most of their time idle waiting on I/O (calls to dozens of backend services to assemble one API response) — non-blocking I/O let a single thread handle thousands of concurrent in-flight requests instead of one-thread-per-request blocking on network waits.
// Blocking (thread held hostage during I/O wait)
User user = userService.getUser(id); // blocks
Orders orders = orderService.getOrders(id); // blocks
// total time = sum of both calls
// Non-blocking / reactive (thread freed during I/O wait)
Mono<User> userMono = userService.getUserAsync(id);
Mono<Orders> ordersMono = orderService.getOrdersAsync(id);
Mono.zip(userMono, ordersMono) // runs concurrently
.map(tuple -> buildResponse(tuple.getT1(), tuple.getT2()));
// total time ≈ max of both calls, not sum
4.5 CDN & Edge Caching
Real-world example: Cloudflare/Akamai/Fastly cache static (and increasingly dynamic, via edge compute) content at ~300 global points of presence. Netflix built their own CDN (Open Connect) — placing caching servers directly inside ISP data centers — because at their traffic volume (>15% of global internet traffic at peak), even paying for third-party CDN was less efficient than owning the edge infrastructure.
Quality Attribute Analysis (Performance patterns as a group):
| Attribute | Assessment |
|---|---|
| ⚡ Performance | Direct, measurable latency/throughput wins — always measure before/after |
| 🔧 Maintainability | Async code is harder to read/debug (callback complexity) — use structured concurrency where possible |
| 📈 Scalability | Non-blocking I/O + pooling directly increases capacity per server |
| 🎯 Optimization | Always profile first (flame graphs, APM tools like Datadog/New Relic) — don’t optimize blind |
Block 5 (Hrs 9–10): Day 4 Lab + Quiz
Hands-On Lab (2 hrs)
- Take an ORM-based endpoint with an N+1 query bug (intentionally write one), profile it with SQL query logging, then fix with eager loading — measure query count before/after.
- Implement token-bucket rate limiting on a sample API and write a test that verifies
429responses under burst traffic. - Configure a JWT auth flow with explicit algorithm whitelisting and short-lived tokens + refresh rotation.
Day 4 Self-Check Quiz
- What’s the difference between OAuth2 and OIDC, and why do most “Login with X” flows need both?
- Name 3 concrete JWT security pitfalls and their fixes.
- Why does Zero Trust reject the idea of a “trusted internal network”? Give the historical incident that motivated Google’s BeyondCorp.
- Explain the N+1 query problem and how eager loading fixes it.
- Why can an oversized connection pool actually make performance worse?
DAY 5 — Scalability, Observability & Real-World System Design Mastery (10 hrs)
Block 1 (Hrs 1–2): Scalability Patterns
1.1 Horizontal vs Vertical Scaling
| Vertical (Scale Up) | Horizontal (Scale Out) | |
|---|---|---|
| Method | Bigger machine (more CPU/RAM) | More machines |
| Ceiling | Hard physical/cost limit | Near-unlimited (in theory) |
| Complexity | Simple — no code changes | Requires stateless design, load balancing |
| Real example | Early Stack Overflow ran on ~9 large servers for years (deliberately) | Google/Amazon run millions of commodity servers |
🎯 Key insight: Stack Overflow’s famous architecture (documented publicly) proved vertical scaling can go remarkably far with disciplined engineering (aggressive caching, minimal abstraction overhead) — horizontal scaling isn’t always necessary at every stage. Choose based on actual bottleneck data, not cargo-culting “everyone does microservices at scale.”
1.2 Statelessness — The Prerequisite for Horizontal Scaling
Any server must be able to handle any request — no in-memory session state pinned to one instance.
BAD: Session stored in server's local memory → "sticky sessions" required → load balancer complexity, uneven load
GOOD: Session stored in Redis/JWT → any server instance can handle any request
1.3 Database Scaling: Replication & Sharding
Read Replicas (vertical read scaling):
Primary DB (writes) → replicates → Read Replica 1, Read Replica 2, Read Replica 3
Application: writes → Primary
reads → round-robin across replicas
Sharding (horizontal scaling — partition data across DBs):
Shard by user_id % 4:
user_id 1,5,9... → Shard 0
user_id 2,6,10... → Shard 1
user_id 3,7,11... → Shard 2
user_id 4,8,12... → Shard 3
Real-world example: Instagram’s early scaling relied heavily on PostgreSQL sharding by user ID — famously documented in their 2012 engineering blog on scaling to 14 million users with a small team, using consistent hashing to distribute users across thousands of logical shards mapped onto a smaller number of physical Postgres instances (to allow re-balancing without re-sharding application code).
🎯 Optimization — Consistent Hashing: Naive hash(key) % N sharding requires re-shuffling almost ALL keys when N changes (adding a server). Consistent hashing (used by DynamoDB, Cassandra, and Instagram’s sharding scheme) only remaps a small fraction of keys when nodes are added/removed.
1.4 Auto-Scaling & Load Balancing Algorithms
| Algorithm | How it works | Best for |
|---|---|---|
| Round Robin | Requests distributed sequentially | Uniform request cost |
| Least Connections | Route to server with fewest active connections | Variable request duration |
| Consistent Hashing | Same client → same server (cache locality) | Session affinity, caching layers |
| Weighted | Route proportional to server capacity | Heterogeneous server fleet |
Block 2 (Hrs 3–4): Resilience at Scale — Backpressure, Rate Limiting, Graceful Degradation
2.1 Backpressure
When a downstream system can’t keep up, it must signal upstream to slow down rather than silently queueing infinitely (which eventually causes OOM crashes).
Real-world example: Reactive Streams specification (used by Akka Streams, Project Reactor at Pivotal/VMware) was created specifically to standardize backpressure signaling across async libraries in the JVM ecosystem — a subscriber tells a publisher “I can only handle 100 more items right now,” preventing fast producers from overwhelming slow consumers.
2.2 Graceful Degradation
Real-world example: During extreme load (e.g., Black Friday), Amazon disables non-critical features (like personalized recommendations) to preserve core checkout functionality — this is a deliberate architectural fallback path, not an accident. The “recommendations” widget architecturally has a well-defined “degrade to empty/generic” behavior baked in from day one, following the circuit breaker fallback pattern from Day 2.
2.3 Load Shedding
When truly overloaded, deliberately reject a percentage of requests (usually the lowest-priority ones) rather than letting the whole system fall over. Google’s SRE book documents this as standard practice — “it is better to serve 95% of requests well than 100% of requests badly (or a total outage).”
Block 3 (Hrs 5–6): Observability — The Three Pillars
3.1 Logging, Metrics, Tracing
| Pillar | Answers | Tools |
|---|---|---|
| Logs | What exactly happened, in detail, at a point in time | ELK Stack, Splunk, Loki |
| Metrics | Aggregate numeric trends over time (latency, error rate, throughput) | Prometheus, Datadog, Grafana |
| Traces | The full path of a single request across multiple services | Jaeger, Zipkin, OpenTelemetry |
Real-world example: Uber built Jaeger (now a CNCF graduated project) because with 1000+ microservices, a single “get an ETA” request might traverse 20+ services — without distributed tracing, finding which one of those 20 added 300ms of latency is nearly impossible. Every request gets a trace ID propagated through headers across every service hop.
// Trace context propagation (simplified)
@GetMapping("/eta")
Mono<ETA> getETA(@RequestHeader("trace-id") String traceId) {
return pricingService.call(traceId) // same trace-id propagated
.then(routingService.call(traceId)) // same trace-id propagated
.map(this::computeETA);
}
3.2 The RED and USE Methods (What to Actually Monitor)
- RED (for services): Rate, Errors, Duration — the 3 metrics that tell you if a service is healthy
- USE (for resources): Utilization, Saturation, Errors — for CPU, memory, disk, network
🎯 Optimization tip: Don’t drown in vanity metrics. Google SRE’s “Four Golden Signals” (Latency, Traffic, Errors, Saturation) is the industry-standard minimum dashboard for any production service.
3.3 SLIs, SLOs, and Error Budgets
Real-world example: Google SRE practice defines an SLO (e.g., “99.9% of requests succeed in under 200ms”) and an error budget (0.1% failure allowance). If the team is within budget, they can ship risky changes fast. If they’ve burned the budget, all further releases freeze until reliability improves — this turns “reliability vs velocity” from a political argument into an objective, data-driven decision framework.
Block 4 (Hrs 7–8): Real-World Architecture Case Studies (Deep Dives)
4.1 Netflix — Chaos Engineering
Netflix’s Chaos Monkey randomly terminates production instances during business hours — not to be reckless, but because it forces every team to architecturally assume failure is constant and design for it (redundancy, circuit breakers, stateless services) rather than hoping failures don’t happen. This directly validates every resilience pattern from Days 2 and 5 in production, continuously, rather than only in theory.
4.2 Uber — From Monolith to 1000s of Microservices, and Back to “Domain-Oriented Microservices”
Uber’s early monolith couldn’t keep up with hyper-growth, so they split aggressively into microservices (reaching 1000s of services by ~2018). But unconstrained proliferation created its own maintainability crisis (too many services, unclear ownership). Their public engineering blog documents a subsequent shift to Domain-Oriented Microservice Architecture (DOMA) — grouping related microservices into “domains” with clear ownership boundaries, essentially re-applying DDD bounded contexts (Day 1) at a higher level of granularity than raw services.
4.3 Amazon — Cell-Based Architecture
To limit blast radius, Amazon (and AWS services like S3, DynamoDB) use cell-based architecture — the entire user base is partitioned into independent “cells,” each a complete, isolated stack (compute, storage, everything). A failure or bad deployment in one cell affects only that cell’s fraction of users, never 100% of traffic. This is bulkheading (Day 2) applied at the infrastructure/regional level, and is documented in AWS’s own “Static stability using Availability Zones” and cell-based architecture whitepapers.
4.4 Discord — Handling Massive Real-Time Fanout
Discord’s engineering blog documents their journey scaling message storage from Postgres → Cassandra (better write scalability for their access pattern) → eventually ScyllaDB — each migration driven by a specific, measured bottleneck (not fashion), demonstrating the Day 3 principle of polyglot persistence chosen by actual workload characteristics.
Block 5 (Hrs 9–10): CAPSTONE — Design a Production E-Commerce Platform
The Brief
Design the architecture for an e-commerce platform expected to handle:
- 5 million daily active users, spiking 20x on flash sales
- Product catalog, cart, checkout, payment, order tracking, recommendations
- 99.95% uptime SLO
- PCI-DSS compliance for payment data
- Global user base (multi-region)
Your Deliverable Should Include:
1. High-level architecture diagram showing:
- API Gateway / BFF layer
- Core bounded-context services (Catalog, Cart, Order, Payment, Inventory, Recommendation, Shipping)
- Data stores per service (justify each choice — SQL vs NoSQL vs cache)
- Message broker for async events (what uses pub/sub vs queue, and why)
2. Written justification covering:
- 📖 Readability/🔧 Maintainability: How are bounded contexts defined? What’s your ADR for the top 3 technology choices?
- ⚡ Performance: Where do you cache? What’s your strategy for the flash-sale 20x spike (auto-scaling triggers, queue-based load leveling)?
- 🔒 Security: How is payment data isolated (PCI scope reduction via tokenization)? What’s your AuthN/AuthZ flow? Where does mTLS apply?
- 📈 Scalability: How do you shard/partition data? What’s stateless vs stateful in your design?
- 🎯 Optimization: Identify your 3 biggest expected bottlenecks and your mitigation for each (e.g., inventory oversell race conditions during flash sales — use optimistic locking or a reservation pattern with TTL)
3. Resilience plan:
- What’s your Saga design for order placement (compensating transactions)?
- Where do circuit breakers and bulkheads go?
- What’s your graceful degradation plan if Recommendation service is down during a flash sale?
4. Observability plan:
- What are your Four Golden Signals dashboards for the Checkout service?
- What’s your SLO and error budget for order placement?
Self-Assessment Rubric
| Criteria | Weak | Expert |
|---|---|---|
| Bounded contexts | Arbitrary service splits | Clear DDD-driven boundaries with justified ownership |
| Data consistency | Assumes distributed transactions work like local ones | Explicit Saga/compensation design for cross-service flows |
| Flash-sale handling | “Just add more servers” | Queue-based leveling, inventory reservation with TTL, load shedding plan |
| Security | Auth mentioned once, generically | PCI scope explicitly minimized, Zero Trust applied to internal calls |
| Trade-offs articulated | None — treats every choice as “best practice” | Explicit trade-offs stated for every major decision (this is what separates architects from pattern-memorizers) |
Final Notes — How to Actually Retain This in 5 Days
- Do every lab. Reading about circuit breakers and watching one trip in your terminal are completely different levels of understanding.
- Write your own ADRs for every decision in the capstone — this is the actual skill architects are hired for, not pattern recall.
- Explain each pattern out loud to someone (or to a rubber duck) — if you can’t explain why Netflix needed Hystrix in one sentence, you haven’t internalized it yet.
- Revisit Day 1 after Day 5. The foundational patterns (Layered, Hexagonal, DDD) will make far more sense once you’ve seen how they scale into Day 2-5’s distributed patterns.
- After this program: read Netflix, Uber, Airbnb, and Discord’s actual public engineering blogs — this document distilled real examples from them, but the primary sources go far deeper and are updated continuously.
You are not done after 5 days — you have the map. Expertise comes from applying these trade-offs under real constraints, ideally on a real system, ideally with the pain of being on-call when one of these patterns is missing.
