Software Architecture Mastery Program (Idea…)

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:

  1. Business requirements (time-to-market, cost)
  2. Quality attributes (the 6 above)
  3. Team structure (Conway’s Law — your system will mirror your org chart)
  4. 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:

  1. Layered architecture (Controller/Service/Repository)
  2. Then refactor further into Hexagonal (define a TaskRepository port, an in-memory adapter for tests, a real DB adapter for production)
  3. 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

  1. Why does Conway’s Law matter when choosing between monolith and microservices?
  2. In Clean Architecture, which direction do dependencies point, and why?
  3. Give an example of an Aggregate and explain why it defines a transaction boundary.
  4. What’s the difference between MVP and MVVM in terms of testability?
  5. 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)

  1. Take 2 services (e.g., OrderService, InventoryService) and put an API Gateway (Spring Cloud Gateway, Kong, or NGINX) in front.
  2. Add a circuit breaker (resilience4j) on the Order→Inventory call. Kill the Inventory service and observe fallback behavior.
  3. Add exponential backoff + jitter to a retry policy and log timestamps to visually confirm the jitter effect.

Day 2 Self-Check Quiz

  1. Why did Segment revert from microservices to a monolith, and what does that tell you about when NOT to use microservices?
  2. Draw the 3 states of a circuit breaker and explain the transition conditions.
  3. Why is jitter necessary in retry logic — what specific failure mode does it prevent?
  4. What problem does BFF solve that a single API Gateway doesn’t?
  5. 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)

  1. Implement a simple Saga (orchestrated) across 3 mock services (Order, Inventory, Payment) with a deliberate payment failure that triggers a compensating action.
  2. Set up Kafka locally (Docker), publish an OrderPlaced event, and have 2 separate consumer groups process it independently.
  3. 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

  1. Why can’t you use a single ACID transaction across 3 microservices, and what pattern replaces it?
  2. What’s the key difference between Choreography and Orchestration Sagas, and when would you pick each?
  3. In CQRS, why might the read model be “eventually consistent,” and is that always acceptable?
  4. Why does Event Sourcing need snapshots, and what problem would you have without them?
  5. 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:

  1. Verify explicitly (every request authenticated — mTLS, short-lived tokens)
  2. Least-privilege access (fine-grained, per-resource, not network-wide)
  3. 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)

  1. Algorithmic complexity (O(n²) → O(n log n)) — biggest wins, free of infra cost
  2. Database (indexing, N+1 queries, connection pooling)
  3. Caching (Day 3)
  4. Concurrency/Async (non-blocking I/O)
  5. Network (CDN, compression, connection reuse)
  6. 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)

  1. 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.
  2. Implement token-bucket rate limiting on a sample API and write a test that verifies 429 responses under burst traffic.
  3. Configure a JWT auth flow with explicit algorithm whitelisting and short-lived tokens + refresh rotation.

Day 4 Self-Check Quiz

  1. What’s the difference between OAuth2 and OIDC, and why do most “Login with X” flows need both?
  2. Name 3 concrete JWT security pitfalls and their fixes.
  3. Why does Zero Trust reject the idea of a “trusted internal network”? Give the historical incident that motivated Google’s BeyondCorp.
  4. Explain the N+1 query problem and how eager loading fixes it.
  5. 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

  1. Do every lab. Reading about circuit breakers and watching one trip in your terminal are completely different levels of understanding.
  2. Write your own ADRs for every decision in the capstone — this is the actual skill architects are hired for, not pattern recall.
  3. 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.
  4. 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.
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *