← All posts

An end-to-end guide to designing, deploying, and operating scalable software on AWS.

The Lifecycle at a Glance

┌──────────────────────────────────────────────────────────────────────┐
│  1. DISCOVER      Problem, users, constraints, success metrics       │
│  2. DESIGN        Domain model, architecture, ADRs, API contracts    │
│  3. BUILD         Code, tests, reviews, local + ephemeral envs       │
│  4. INTEGRATE     CI: build once, test, scan, produce one artifact   │
│  5. DEPLOY        CD: promote that artifact Dev → QA → Staging → Prod│
│  6. RELEASE       DNS, CDN, TLS, WAF, progressive rollout            │
│  7. OPERATE       Monitor, alert, on-call, incident response         │
│  8. EVOLVE        Feedback, scale, refactor, pay down debt           │
└──────────────────────────────────────────────────────────────────────┘
        ▲                                                        │
        └────────────────── continuous feedback ─────────────────┘

The loop matters more than the sequence. Mature teams run all eight phases continuously rather than as a one-time waterfall.


PHASE 1 — Discovery and Product Definition

Before any architecture, the team establishes what is actually being built and how success will be measured.

1.1 What Gets Produced

Artifact Purpose
Problem statement The user pain, in one paragraph, without a proposed solution
User personas & journeys Who uses it, what they are trying to accomplish
Functional requirements What the system must do
Non-functional requirements (NFRs) The numbers that drive architecture
Success metrics How you know it worked (activation, retention, conversion)
Constraints Budget, deadline, compliance, team size, existing systems

1.2 Non-Functional Requirements Drive Everything

This is the step teams most often skip, and it is the one that determines the architecture. Get concrete numbers:

Scale
    Expected users:            100k registered, 5k concurrent peak
    Requests per second:       2,000 average / 12,000 peak
    Data volume:               500 GB year 1, growing 40% annually
    Traffic shape:             3x spike during promotional campaigns

Performance
    API latency:               p50 < 100ms, p95 < 300ms, p99 < 800ms
    Page load (LCP):           < 2.5s on 4G
    Search results:            < 500ms

Availability
    Uptime target:             99.9% (≈43 min downtime/month)
    RTO (recovery time):       < 1 hour
    RPO (data loss tolerance): < 5 minutes

Security & Compliance
    PII handling, PCI-DSS for payments, GDPR right-to-erasure,
    data residency requirements, audit retention period

Cost
    Target infrastructure spend: $X/month at launch scale

Every one of these numbers maps to an AWS decision later. "99.9% uptime" means multi-AZ. "RPO < 5 minutes" means point-in-time recovery and cross-region replication. "3x campaign spikes" means autoscaling and a caching strategy. Without the numbers, architecture becomes guesswork or cargo-culting.

1.3 Deliberately Choosing Boring

A key industry discipline: innovation tokens are finite. Spend novelty on the parts that differentiate the product, and choose proven, well-understood technology everywhere else. A team that picks a novel database, a novel language, a novel deployment model, and a novel frontend framework simultaneously spends its entire first year fighting tooling rather than shipping.


PHASE 2 — Architecture and System Design

2.1 Domain Modelling First, Services Second

Microservice boundaries follow business capabilities, not technical layers. The standard approach is Domain-Driven Design:

  1. Event storming — map the business events end to end (OrderPlaced, PaymentCaptured, ShipmentDispatched).
  2. Identify bounded contexts — clusters of events and rules that change together and share a vocabulary.
  3. Define the ubiquitous language — the same word means one thing inside a context. "Customer" in Billing (a payment method holder) is not "Customer" in Support (a ticket author). Forcing them into one shared model is a classic source of coupling.
  4. Draw context maps — how contexts interact: upstream/downstream, published events, anti-corruption layers.

A correct boundary means a typical feature request touches one service. If most features require coordinated changes across five services, the boundaries are wrong — you have a distributed monolith, which combines the operational cost of microservices with the coupling of a monolith.

2.2 Monolith or Microservices?

The honest answer for most new products: start with a well-structured modular monolith.

Modular Monolith                        Microservices
──────────────────                      ──────────────
One deployable                          Many deployables
In-process calls (fast, reliable)       Network calls (latency, partial failure)
One database, ACID transactions         Per-service data, eventual consistency
Refactor boundaries cheaply             Boundaries are expensive to move
One pipeline, one runtime to operate    N pipelines, service mesh, tracing required
Team of 3–15 works fine                 Needs platform investment + team autonomy

Split into services when you have a concrete reason:

  • Independent scaling — search needs 20 instances, billing needs 2
  • Team autonomy — separate teams blocked by a shared release train
  • Fault isolation — a failing recommendations engine must not take checkout down
  • Divergent technology — one workload genuinely needs a different runtime
  • Divergent compliance — payments needs a stricter, isolated environment

Never split because "microservices are best practice." Premature decomposition is one of the most expensive mistakes in software, because distributed boundaries are far harder to move than in-process ones.

The realistic path: modular monolith → extract the one or two services with real independent-scaling or isolation needs → extract further only as pain demands.

2.3 Reference Architecture on AWS

                        Users (browser / mobile)
                                 │
                         Route 53 (DNS, health-based routing)
                                 │
                         CloudFront (CDN, global edge)
                     ┌───────────┴────────────┐
                     │                        │
              Static frontend           Dynamic API
              S3 (SPA bundle)                 │
                                          AWS WAF
                                              │
                              ┌───────────────┴──────────────┐
                              │   API Gateway  or  ALB       │
                              │   authn/z, rate limit, route │
                              └───────────────┬──────────────┘
                                              │
        ┌──────────────┬────────────┬─────────┴────────┬────────────────┐
        ▼              ▼            ▼                  ▼                ▼
   Identity Svc   Catalog Svc   Order Svc         Payment Svc     Notification Svc
   (ECS/EKS)      (ECS/EKS)     (ECS/EKS)         (ECS/EKS)       (Lambda)
        │              │            │                  │                │
        │         ┌────┴────┐       │                  │                │
        │         │ElastiCache      │                  │                │
        │         │ (Redis) │       │                  │                │
        │         └─────────┘       │                  │                │
        ▼              ▼            ▼                  ▼                ▼
   RDS/Aurora    RDS + OpenSearch  Aurora          Aurora           (stateless)
   (Postgres)                      + DynamoDB      (encrypted)
                                        │
                                        ▼
                     ┌──────────────────────────────────────┐
                     │  EventBridge / SNS / SQS / Kinesis   │  ← async backbone
                     └──────────────────────────────────────┘
                                        │
                     ┌──────────────────┴───────────────────┐
                     │ S3 (objects, data lake)              │
                     │ Secrets Manager / KMS                │
                     │ CloudWatch / X-Ray / OpenTelemetry   │
                     └──────────────────────────────────────┘

2.4 Synchronous vs Asynchronous Communication

The most consequential design choice inside a distributed system.

SYNCHRONOUS (REST / gRPC)
    Caller waits for a response.
    ✔ Simple, immediate consistency, easy to reason about
    ✘ Temporal coupling — callee down means caller down
    ✘ Latency compounds: 5 chained calls at 100ms = 500ms
    ✘ Cascading failure risk

    Use for: queries, user-facing reads, operations needing
             an immediate answer (auth check, price lookup)

ASYNCHRONOUS (events / queues)
    Caller publishes and moves on.
    ✔ Temporal decoupling — consumer can be down, work is buffered
    ✔ Natural buffering absorbs traffic spikes
    ✔ Easy to add new consumers without touching the producer
    ✘ Eventual consistency; harder debugging; needs idempotency

    Use for: side effects, fan-out, long-running work, integration
             between bounded contexts

Practical rule: synchronous within a bounded context; asynchronous between bounded contexts.

Checkout illustrates this well:

SYNCHRONOUS (user is waiting — must succeed now)
    validate cart → check inventory → authorize payment → create order
                                                              │
                                                              ▼
                                             publish OrderPlaced event
                                                              │
ASYNCHRONOUS (user does not need to wait) ────────────────────┤
    ├─▶ send confirmation email
    ├─▶ decrement inventory
    ├─▶ notify warehouse
    ├─▶ update analytics
    └─▶ trigger loyalty points

If the email provider is down, the order still succeeds. That isolation is the entire point.

2.5 Distributed Data Patterns

Once each service owns its own database, familiar guarantees disappear. The patterns that replace them:

Saga — a business transaction spanning services, implemented as a sequence of local transactions with compensating actions on failure.

Order Created → Payment Charged → Inventory Reserved → Shipment Booked
                                          │ FAILS
                                          ▼
                             Compensate:  Refund Payment
                                          Cancel Order

Choreography (services react to each other's events) is simpler for short flows; orchestration (a coordinator like AWS Step Functions drives the flow) is clearer for long or complex ones.

Outbox pattern — writing to your database and publishing an event are two separate systems; without care you can commit one and lose the other. Instead, write the event into an outbox table in the same local transaction as the business data, then have a relay publish it. This guarantees at-least-once delivery without distributed transactions.

Idempotency — because at-least-once delivery means duplicates are inevitable, every consumer must tolerate replays. Standard technique: an idempotency key per operation, recorded on first processing and checked thereafter.

CQRS — separate the write model from read models when their shapes genuinely diverge (e.g. normalised writes in a relational DB, denormalised search-optimised reads in OpenSearch).

API composition / BFF — a Backend-for-Frontend aggregates calls to several services so the client makes one request instead of seven. Prevents chatty mobile clients and leaks of internal service topology.

2.6 API Contract Design

  • Contract-first. Define the OpenAPI/AsyncAPI/protobuf schema before implementing. It unblocks frontend and backend to work in parallel and lets you generate mocks.
  • Version from day one (/v1/... or header-based). You will need v2 sooner than you expect.
  • Backward compatibility is mandatory while any client still uses the old version. Additive changes only; never repurpose a field's meaning.
  • Consumer-driven contract tests verify that a provider change does not break real consumers, catching integration breaks in CI rather than in staging.
  • Standardise the cross-cutting concerns once, org-wide: error envelope, pagination, filtering, correlation-ID header, auth scheme, rate-limit headers.

2.7 Recording Decisions — ADRs

Every significant choice gets a short Architecture Decision Record committed alongside the code:

# ADR-014: Use Aurora PostgreSQL for the Order Service

Status:   Accepted (2026-03-11)
Context:  Orders require multi-row ACID transactions, complex reporting
          joins, and an RPO under 5 minutes. Team has deep SQL experience.
Decision: Aurora PostgreSQL, Multi-AZ, with point-in-time recovery.
Alternatives considered:
          - DynamoDB — rejected: transactional/reporting query patterns
            would require significant denormalisation and app-side joins
          - Self-managed PostgreSQL on EC2 — rejected: operational burden
Consequences:
          + Familiar tooling, strong consistency, mature ecosystem
          − Higher cost than DynamoDB at low volume
          − Requires connection pooling (RDS Proxy) as services scale out

Six months later, ADRs answer "why on earth did we do it this way?" without archaeology. They are cheap to write and enormously valuable.


PHASE 3 — Data Layer Design

3.1 Polyglot Persistence — Right Tool per Job

Each service picks the store matching its access pattern. On AWS:

Need AWS service Use when
Relational, ACID, complex queries RDS / Aurora (PostgreSQL, MySQL) Orders, payments, users — anything with invariants across rows
Key-value / document at massive scale DynamoDB Sessions, carts, event logs, high-write telemetry; predictable access patterns
Full-text & faceted search OpenSearch Product search, autocomplete, log analytics
In-memory cache / ephemeral state ElastiCache (Redis / Valkey) Hot reads, sessions, rate limiting, leaderboards
Object / blob storage S3 Images, documents, exports, backups, data lake
Time series Timestream IoT, metrics, sensor data
Graph Neptune Recommendations, fraud rings, social graphs
Analytics / warehouse Redshift or Athena over S3 BI, reporting, ad-hoc analysis
Wide-column Keyspaces (Cassandra) Very high write throughput, time-partitioned

Critical rule: one service owns its database. No other service reads it directly. Shared databases are the fastest way to recreate a monolith with extra network hops — a schema change becomes a cross-team negotiation, and you lose every benefit of independent deployment.

3.2 Relational vs NoSQL — the Real Decision

Choose RELATIONAL when:
    - Data is highly relational with meaningful joins
    - You need multi-row/multi-table ACID transactions
    - Query patterns will evolve unpredictably (ad-hoc reporting)
    - Strong consistency is a business requirement (money, inventory)
    - Team knows SQL (almost always true)

Choose NoSQL (DynamoDB) when:
    - Access patterns are known, simple, and stable
    - You need single-digit-millisecond latency at extreme scale
    - Write volume exceeds what a single writer node can absorb
    - The schema is genuinely flexible/sparse
    - You want zero operational overhead and true pay-per-request

DynamoDB rewards designing the table around the queries (single-table design, careful partition keys). It punishes ad-hoc querying. If you cannot enumerate your access patterns up front, that is strong evidence for relational.

3.3 Reliability and Recovery

Multi-AZ deployment
    Primary in AZ-a, synchronous standby in AZ-b.
    Automatic failover in 30–120s. Non-negotiable for production.

Read replicas
    Offload reporting and read-heavy traffic from the primary.
    Note: replicas are eventually consistent — never read your own
    write from a replica without care (the read-after-write trap).

Automated backups + point-in-time recovery
    Restore to any second within the retention window.

Cross-region replication (Aurora Global Database)
    For strict RTO/RPO or regional DR requirements.

RESTORE DRILLS
    An untested backup is not a backup. Schedule real restore
    exercises — quarterly at minimum. Teams routinely discover
    their backups were unusable only during an actual incident.

3.4 Schema Migrations Without Downtime

Migrations are the most common cause of failed deploys. Use expand/contract and never combine a destructive change with the code depending on it:

Step 1  ADD the new column (nullable, with default)   ← old code unaffected
Step 2  Deploy code that writes to BOTH old and new
Step 3  Backfill historical rows (batched, throttled)
Step 4  Deploy code that reads only the new column
Step 5  Stop writing the old column
Step 6  DROP the old column                            ← only now

Each step is independently deployable and independently reversible. Compressing this into one release makes rollback impossible: the moment the old column is gone, the previous application version cannot run.

Also: migrations run as a distinct, gated pipeline step — not on application startup, where N instances would race each other.

3.5 Connection Management

A frequently missed failure mode. Each service instance holds a connection pool; autoscaling to 50 instances × a 20-connection pool = 1,000 connections, which will exhaust the database long before CPU becomes the constraint.

  • Use RDS Proxy (or an equivalent pooler) to multiplex connections
  • Size pools deliberately; more connections is not more throughput
  • Set aggressive connection and statement timeouts
  • Serverless/Lambda workloads especially need a proxy — otherwise each cold start opens a new connection

PHASE 4 — Caching Strategy

Caching is the highest-leverage performance work available, and the easiest to get subtly wrong.

4.1 The Caching Layers

① BROWSER CACHE           Cache-Control, ETag on static assets
       │                  Cheapest possible request: none at all
       ▼
② CDN — CloudFront        Static assets, images, cacheable API GETs
       │                  Served from an edge location near the user
       ▼
③ API GATEWAY CACHE       Response caching per endpoint + key
       │
       ▼
④ APPLICATION CACHE       In-process memory (fast, per-instance,
       │                  inconsistent across instances — use only
       │                  for small, rarely-changing reference data)
       ▼
⑤ DISTRIBUTED CACHE       ElastiCache (Redis) — shared across all
       │                  instances: sessions, hot entities,
       │                  computed aggregates, rate-limit counters
       ▼
⑥ DATABASE CACHE          Buffer pool, query cache, materialized views
       │
       ▼
⑦ ORIGIN                  The actual computation / storage

Every layer you satisfy a request at is a layer of cost, latency, and load you avoid below it.

4.2 Caching Patterns

Cache-Aside (lazy loading) — the default, used ~80% of the time:

read(key):
    value = cache.get(key)
    if value exists:  return value           # hit
    value = database.query(key)              # miss
    cache.set(key, value, ttl)
    return value

Only requested data is cached; a cache failure degrades performance rather than breaking correctness. Downside: every miss pays the full latency, and the first request after a deploy is always slow.

Write-Through — write to cache and database together. Cache is always fresh; writes are slower, and you cache data that may never be read.

Write-Behind — write to cache, flush to the database asynchronously. Very fast writes, but data loss risk if the cache dies before flush. Use only where that loss is acceptable.

Refresh-Ahead — proactively refresh hot keys before expiry, so users never pay the miss penalty on popular items.

4.3 Invalidation — the Hard Part

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

Strategies, in rough order of preference:

  1. TTL-based expiry — simplest and most robust. Accept bounded staleness. Ask the business how stale is acceptable; the answer is usually "a few minutes", which TTL handles trivially.
  2. Event-driven invalidation — the service publishes ProductUpdated, consumers evict the key. Accurate, but adds coupling and can miss events.
  3. Versioned keysproduct:v7:123. Bumping the version invalidates everything atomically; old entries age out naturally. Excellent for schema changes.
  4. Write-through — no invalidation needed because the cache is updated at write time.

Never rely on manual invalidation ("we'll remember to clear it"). It will be forgotten.

4.4 Cache Failure Modes to Design Against

Thundering herd / cache stampede — a popular key expires and 5,000 concurrent requests all miss and hit the database simultaneously, often taking it down.

Mitigations: lock or single-flight so only one request recomputes while others wait; add random jitter to TTLs so keys do not expire in lockstep; refresh-ahead for known-hot keys.

Cache penetration — repeated requests for a key that does not exist bypass the cache entirely every time (sometimes maliciously).

Mitigation: cache the negative result with a short TTL; use a bloom filter for existence checks.

Cache avalanche — a large set of keys expires at once, or the cache cluster restarts cold, and full traffic hits the origin.

Mitigations: jittered TTLs, cache warming on deploy, circuit breakers protecting the origin, multi-AZ Redis with replicas.

Stale-data bugs — the most damaging category, because they are silent. A user updates their profile and still sees the old value; a price change does not propagate. Be explicit about which data can tolerate staleness and which cannot. Never cache authorization decisions or account balances with a long TTL.

4.5 What to Cache — and What Not To

GOOD CACHE CANDIDATES
    Read-heavy, write-light data      (product catalog, categories)
    Expensive computations            (aggregates, recommendations)
    Reference/lookup data             (countries, tax rates, config)
    Session data                      (naturally keyed, short-lived)
    Rendered fragments                (rarely-changing page sections)
    Rate-limit counters               (Redis is ideal for this)

POOR CACHE CANDIDATES
    Data written more often than read (cache churn, no benefit)
    Highly personalized, low-reuse    (near-zero hit rate)
    Financial balances / inventory    (staleness has real cost)
    Authorization decisions           (revocation must be immediate)
    Anything where staleness is a correctness bug

Always measure the hit rate. A cache below ~80% hit rate for hot paths is usually mis-keyed, under-sized, or caching the wrong things — and it may be adding latency rather than removing it. Track hit rate, eviction rate, and memory pressure as first-class metrics.


PHASE 5 — Development

5.1 Environments

LOCAL         Developer machine. Containerised dependencies
              (database, cache, message broker) so setup is one command.
              LocalStack or equivalent can emulate AWS services.

EPHEMERAL/PR  Spun up per pull request, destroyed on merge.
              Reviewers and QA see the change running, not a screenshot.
              Highest-value environment investment a team can make.

DEV           Shared integration environment. Auto-deploys on merge.
              Expected to be occasionally broken.

QA / STAGING  Production-like: same topology, same instance classes
              (scaled down), anonymised production-shaped data.
              This is where load tests and release candidates live.

PRODUCTION    The real thing.

Enforce environment parity: same artifact, same infrastructure code, same runtime versions. Everything that differs is configuration.

5.2 Source Control and Review

  • Trunk-based development with short-lived branches (hours to a couple of days). Long-lived feature branches produce painful merges and hide integration problems until the worst possible moment.
  • Small pull requests. Review quality collapses above ~400 changed lines; a 2,000-line PR gets "LGTM" and nothing else.
  • Required checks before merge: build, tests, coverage threshold, linting, dependency and secret scanning, at least one approving review.
  • Protected main branch, no direct pushes.
  • Conventional commits so changelogs and semantic versions can be generated automatically.

5.3 Feature Flags — Decoupling Deploy from Release

The technique that makes trunk-based development and continuous deployment practical:

Deploy  =  the code is running in production   (a technical event)
Release =  users can see the behaviour          (a business event)

Incomplete work ships behind a disabled flag. This enables:

  • Merging continuously instead of maintaining long branches
  • Progressive rollout: internal staff → 1% → 10% → 50% → 100%
  • Instant kill switch — turning a flag off is far faster and safer than a rollback deploy
  • A/B experiments and per-tenant enablement
  • Decoupling launch timing from deployment timing

Discipline required: flags are technical debt with a half-life. Every flag needs an owner and a removal date, or the codebase accumulates hundreds of dead conditionals and an untestable combinatorial explosion of states.

5.4 Testing Strategy

        ▲   Few · slow · expensive · realistic
        │
        │   ┌─────────────────────────────┐
        │   │   Manual exploratory        │  Judgement, not regression
        │   ├─────────────────────────────┤
        │   │   E2E (critical journeys)   │  Signup, checkout, payment
        │   ├─────────────────────────────┤
        │   │   Contract tests            │  Service boundaries hold
        │   ├─────────────────────────────┤
        │   │   Integration tests         │  Real DB/cache in containers
        │   ├─────────────────────────────┤
        │   │   Unit tests                │  Business logic, fast, many
        │   └─────────────────────────────┘
        │
        ▼   Many · fast · cheap · isolated

Additional gates for a production-grade system:

  • Load tests against staging, sized to the NFR targets — before launch, and before any expected traffic event
  • Chaos experiments — kill instances, inject latency, sever a dependency, verify graceful degradation
  • Security tests — SAST, DAST, dependency CVE scanning, periodic penetration testing
  • Accessibility tests — automated WCAG checks in CI plus manual screen-reader passes
  • Smoke tests post-deploy against the real environment

Test the critical revenue path obsessively. If checkout breaks, nothing else matters.

5.5 Twelve-Factor Application Discipline

Non-negotiable for anything running on cloud infrastructure:

  • Config in the environment, never in the image or the repository
  • Stateless processes — any instance can serve any request; state lives in a database, cache, or object store. This is what makes horizontal scaling and instance replacement possible.
  • Backing services are attached resources — swapping a database is a config change
  • Logs to stdout as structured JSON; the platform handles collection
  • Graceful shutdown — handle the termination signal, drain in-flight requests, finish or requeue background work
  • Disposability — fast startup, safe termination; instances are cattle, not pets
  • Dev/prod parity — minimise the gap in time, personnel, and tooling

PHASE 6 — Build and Continuous Integration

6.1 The Pipeline

Commit / PR
    │
    ├─ Lint + format check          ~10s   ┐
    ├─ Unit tests                   ~2min  │ fail fast — cheapest first
    ├─ Build artifact               ~3min  │
    ├─ SAST + secret scan           ~1min  ┘
    ├─ Build container image        ~2min
    ├─ Image vulnerability scan     ~1min   ← block on HIGH/CRITICAL
    ├─ Integration tests            ~5min
    ├─ Contract tests               ~1min
    │
    ▼
Push to ECR  →  ONE immutable artifact, tagged with the commit SHA

Target under ~10 minutes for the whole CI run. Beyond that, developers context-switch away and the feedback loop breaks.

6.2 Build Once, Promote Everywhere

The single most important CI/CD rule, repeated because it is violated so often:

                    myapp@sha256:9f2c4a...
                            │
        ┌───────────────────┼───────────────────┐
        ▼                   ▼                   ▼
      DEV                  QA               PRODUCTION
   (env config)       (env config)         (env config)

The bytes tested in QA are the bytes running in Production. Rebuilding per environment means QA validated an artifact that never shipped — dependency resolution, base image patches, and build-time nondeterminism all drift between builds.

6.3 Infrastructure as Code

All infrastructure defined in version-controlled code — Terraform, CDK, CloudFormation, or Pulumi.

✔ Reviewable        infrastructure changes go through pull request
✔ Reproducible      recreate an entire environment from scratch
✔ Auditable         git log answers "who opened that security group?"
✔ Consistent        Dev/QA/Prod from the same modules, different variables
✔ Disaster recovery rebuild a region from source

Rules that matter in practice:

  • No manual console changes in production. Ever. Console drift is invisible, undocumented, and lost on the next apply. Grant read-only console access in production; make changes through the pipeline.
  • Remote state with locking (S3 + DynamoDB for Terraform)
  • Plan output posted to the PR so reviewers see what will actually change
  • Modules per environment, parameters per environment — never copy-pasted stacks that silently diverge
  • Drift detection running on a schedule

6.4 Deployment Strategies

ROLLING            Replace instances gradually.
                   Default for ECS/EKS. No downtime.
                   Both versions run at once → requires backward-
                   compatible schema and API changes.

BLUE-GREEN         Stand up the full new version alongside the old,
                   switch the load balancer, keep the old warm.
                   Instant rollback; ~2x cost during the switch.
                   AWS CodeDeploy automates this.

CANARY             Route 5% → 25% → 100%, watching error rate and
                   latency at each step, with automatic rollback on
                   alarm. Lowest risk; needs good metrics.
                   The industry standard for high-traffic systems.

FEATURE FLAG       Deploy dark, enable progressively per user segment.
                   Fastest possible kill switch.

Mature setups combine canary deployment with feature flags: the code rolls out safely, and the behaviour is enabled independently.

6.5 Progressive Delivery with Automatic Rollback

The deployment gate that turns "we deployed and hoped" into an engineering control:

Deploy canary (5% traffic)
    │
    ├─ Watch for 10 minutes:
    │     error rate  < baseline + 0.5%
    │     p99 latency < baseline + 20%
    │     no new alarm states
    │
    ├─ Breached?  ──▶ automatic rollback, page on-call, stop the pipeline
    │
    └─ Healthy?   ──▶ 25% → observe → 50% → observe → 100%

Automating the rollback decision removes the worst failure mode in deployment: a human under pressure deciding whether the graphs look "bad enough" at 2 a.m.


PHASE 7 — AWS Infrastructure and Deployment

7.1 Account Structure

Production-grade AWS starts with multiple accounts, not one account with tags:

AWS Organization
├── Management account         (billing, SCPs — no workloads)
├── Security / Log Archive     (CloudTrail, GuardDuty, immutable logs)
├── Shared Services            (CI/CD, artifact registries, DNS)
├── Development                (permissive, low limits, auto-shutdown)
├── QA / Staging               (production-like)
└── Production                 (locked down, minimal human access)

Separate accounts give a hard blast radius boundary — a mistake in Dev physically cannot touch Production — plus clean cost attribution and independent service quotas. Service Control Policies enforce guardrails organisation-wide (e.g. deny disabling CloudTrail, deny unapproved regions).

7.2 Network Design

VPC  (10.0.0.0/16)  — spanning at least 3 Availability Zones

    PUBLIC SUBNETS        ALB, NAT Gateway, bastion (if any)
                          Internet-routable

    PRIVATE SUBNETS       Application containers/instances
                          Outbound via NAT only, no inbound from internet

    ISOLATED SUBNETS      Databases, caches
                          No internet route at all
  • Security groups are the real firewall — reference other security groups rather than CIDR ranges (allow 5432 from sg-app), so rules stay correct as instances change.
  • VPC endpoints for S3, DynamoDB, ECR, Secrets Manager keep traffic on the AWS network — lower latency, lower cost, and it avoids NAT Gateway charges, which are a notorious surprise on the bill.
  • Multi-AZ everything. An AZ is a real failure domain; single-AZ deployment forfeits the availability target you promised in Phase 1.
  • Multi-region only when the requirement genuinely demands it — it multiplies complexity in data consistency, deployment, and cost.

7.3 Choosing Compute

Option Best for Trade-off
Lambda Event-driven, spiky, low-duration work; glue code Cold starts, duration limits, harder local testing
ECS on Fargate Standard long-running services Simplest container path on AWS; less control than EKS
EKS (Kubernetes) Complex platforms, multi-team, portability needs Powerful, portable — but a genuine platform-team commitment
EC2 Auto Scaling Specialised or licensed workloads Most control, most operational burden
App Runner / Amplify Small teams, simple web apps Fastest to ship, least flexible

The honest recommendation: most teams should default to ECS on Fargate. It gives containerised, autoscaled, multi-AZ services without a dedicated platform team. Adopt EKS when there is a concrete need — heavy multi-tenancy, a rich operator ecosystem, or multi-cloud portability — not because Kubernetes is fashionable. Kubernetes is a full-time responsibility, not a deployment target.

Use Lambda for genuinely event-driven work (S3 triggers, scheduled jobs, stream processing, webhooks) rather than as a general application runtime.

7.4 Autoscaling

HORIZONTAL (add instances) ← strongly preferred
    Scale on the metric that reflects real load:
      - request count per target
      - queue depth (for workers — the best signal available)
      - p95 latency
      - CPU/memory (crude, but a reasonable default)

    Scale OUT fast, scale IN slow.
    Aggressive scale-in causes thrash and drops in-flight work.

VERTICAL (bigger instances)
    Simpler but bounded, and usually requires a restart.

SCHEDULED
    Pre-warm ahead of known events — business-hours ramp, campaign
    launches, Black Friday. Reactive autoscaling always lags a spike;
    scheduled capacity meets it.

Load-test to find the real breaking point rather than guessing thresholds, and remember to scale the layers behind the application — database connections, cache capacity, and third-party API quotas frequently become the true ceiling.

7.5 Going Public

The final steps that put the system on the open internet:

DNS            Route 53 hosted zone, health checks, latency or
               failover routing policies

TLS            ACM certificate (free, auto-renewing), TLS 1.2+ only,
               HSTS enabled

CDN            CloudFront in front of both static assets and the API
               Compression, HTTP/3, sensible cache policies,
               Origin Access Control so S3 is not publicly readable

WAF            Managed rule sets (OWASP Top 10, bad inputs, bot control),
               rate-based rules, geo restrictions if applicable

DDoS           Shield Standard is automatic; Shield Advanced for
               high-value targets

EDGE LOGIC     CloudFront Functions / Lambda@Edge for redirects,
               header manipulation, simple auth at the edge

Pre-launch checklist worth running explicitly: load test passed at target scale, alerts configured and tested, runbooks written, on-call rota staffed, rollback rehearsed, backups verified by an actual restore, security review completed, cost alarms set, and a status page ready.


PHASE 8 — Observability and Monitoring

You cannot operate what you cannot see. Monitoring tells you that something broke; observability lets you work out why without shipping new code.

8.1 The Three Pillars — Plus One

METRICS      Numeric time series. Cheap, aggregatable, alertable.
             "p99 latency is 800ms", "error rate is 2.3%"
             → CloudWatch, Managed Prometheus

LOGS         Discrete structured events with full context.
             "Order 4471 failed: payment gateway timeout after 3 retries"
             → CloudWatch Logs, OpenSearch

TRACES       The path of one request across every service.
             "Checkout took 2.1s: 1.8s waiting on the inventory service"
             → X-Ray, OpenTelemetry

PROFILES     Where CPU and memory actually go inside a process.
             → CodeGuru Profiler, continuous profiling tools

Use OpenTelemetry as the instrumentation standard. It is vendor-neutral, so the backend can change without re-instrumenting every service — which matters, because observability vendors get expensive and teams do switch.

8.2 Correlation IDs — the Prerequisite

Without this, distributed debugging is guesswork. Generate a correlation/trace ID at the edge, propagate it through every synchronous call and every asynchronous message, and include it in every log line and every error response.

Client → [trace: abc123] → API Gateway
                         → Order Service      [trace: abc123]
                         → Payment Service    [trace: abc123]
                         → SQS message        [trace: abc123]
                         → Notification Svc   [trace: abc123]

Then one query reconstructs the entire failed request across every service and every queue hop. Returning the trace ID to the client also means a user-reported problem arrives with the exact key needed to investigate it.

8.3 What to Actually Monitor

The Four Golden Signals — the standard starting point for every service:

LATENCY        Response time distribution (p50/p95/p99 — never the mean;
               averages conceal exactly the tail users complain about).
               Track successful and failed requests separately, because
               fast failures otherwise flatter your latency graph.

TRAFFIC        Requests per second, active users, queue depth

ERRORS         Explicit failures (5xx) and implicit ones (200 responses
               with a wrong or empty body)

SATURATION     How full the system is: CPU, memory, connection pool
               utilisation, queue backlog, disk. The leading indicator —
               saturation rises before latency and errors do.

Layer the monitoring:

Layer Watch
Business Orders/minute, signup rate, revenue, cart abandonment, payment success rate
Application Golden signals per endpoint, dependency call health, cache hit rate
Infrastructure CPU, memory, disk, network, container restarts, task placement failures
Data Replication lag, connection count, slow queries, deadlocks, storage growth
External Third-party API latency and error rate, certificate expiry, quota consumption

Business metrics are the most valuable alerts you have. A drop in orders-per-minute catches broken checkouts that every infrastructure metric reports as perfectly healthy — the servers are fine, they are just serving a broken flow.

8.4 SLIs, SLOs, and Error Budgets

The framework that converts "the site feels slow" into an engineering decision:

SLI  (Indicator)  What you measure
                  "% of requests served successfully in < 300ms"

SLO  (Objective)  The target
                  "99.9% of requests over a rolling 30 days"

ERROR BUDGET      The permitted failure: 100% − 99.9% = 0.1%
                  ≈ 43 minutes of failure per month

    Budget remaining  →  ship features, take risks
    Budget exhausted  →  freeze features, work on reliability

This makes reliability a shared, quantified conversation rather than an argument between product and engineering. It also prevents over-engineering: if the budget is consistently untouched, the system is more reliable than it needs to be, and that surplus is being paid for in velocity and infrastructure cost.

8.5 Alerting That People Trust

PAGE (wake someone)     User-visible impact requiring immediate action
                        Checkout failing · site down · data loss risk
                        · error budget burning fast

TICKET (business hours) Degradation with headroom
                        Disk at 80% · elevated retries · one node unhealthy

DASHBOARD ONLY          Informational; no notification

Rules for alerts that stay useful:

  • Alert on symptoms, not causes. "Checkout error rate > 2%" beats "CPU > 80%" — high CPU may be harmless, while users failing to check out never is.
  • Every page must be actionable and have a runbook. If the responder's only action is to acknowledge it, delete the alert.
  • Alert fatigue is a real outage risk. A team that receives 40 noisy pages a week will miss the one that matters. Ruthlessly tune or delete noisy alerts.
  • Use multi-window burn-rate alerts on the error budget rather than raw thresholds: page on a fast burn, ticket on a slow burn.
  • Test alerts — deliberately break something in staging and confirm the page actually arrives at the right phone.

8.6 Dashboards

Build for the audience:

  • Service dashboard — golden signals, dependencies, deploy markers overlaid on the graphs (so "when did this start?" is answered instantly)
  • Business dashboard — funnel, revenue, active users
  • On-call dashboard — one screen answering "is anything broken and where?"
  • Cost dashboard — spend by service, trend, anomalies

Annotating deploys on every graph is a small change that resolves a large share of incident investigations in seconds.


PHASE 9 — Reliability Engineering

9.1 Designing for Failure

In a distributed system, failure is the steady state, not the exception. Every remote call must assume the other side can be slow, wrong, or absent.

Timeouts — every network call has one. A missing timeout means one slow dependency exhausts your threads and takes down a healthy service. Timeouts should tighten as you go deeper in the call stack.

Retries with exponential backoff and jitter — retry only idempotent, transient failures. Naïve immediate retries turn a brief blip into a self-inflicted DDoS as every client retries in perfect synchrony; jitter spreads them out. Cap total attempts.

Circuit breakers — after N consecutive failures, stop calling the failing dependency and fail fast, then probe periodically for recovery. This prevents one sick service from consuming every caller's thread pool and cascading a single failure system-wide.

Bulkheads — isolate resource pools per dependency so exhaustion in one path cannot starve the others.

Graceful degradation — decide in advance what "reduced but working" means. Recommendations unavailable → show bestsellers. Search down → show the category tree. Never let a non-critical dependency break a critical path.

Dead letter queues — messages that fail repeatedly go to a DLQ for inspection rather than blocking the queue forever. Alert on DLQ depth; a filling DLQ is a silent, ongoing data-loss event.

Idempotency everywhere — retries and at-least-once delivery guarantee duplicates. Every mutating operation should be safe to execute twice.

9.2 Disaster Recovery

Backups              Automated, encrypted, cross-region copies,
                     retention matching compliance requirements

Restore drills       Quarterly at minimum, timed against the stated RTO.
                     An untested backup is a hope, not a plan.

Runbooks             Step-by-step recovery procedures, written before
                     the incident, tested by someone who did not write them

Game days            Deliberately break production-like systems and
                     practise the response as a team

Chaos engineering    Continuously inject failure (AWS Fault Injection
                     Simulator) to prove resilience holds as the
                     system changes

DR strategies, cheapest to fastest: backup-and-restore (hours) → pilot light (tens of minutes) → warm standby (minutes) → active-active (seconds, at roughly double the cost). Pick based on the RTO/RPO committed in Phase 1, not on ambition.

9.3 Incident Response

DETECT      Automated alert (ideally before any customer notices)
TRIAGE      Assess severity, page the right people, open a channel
MITIGATE    Restore service FIRST — roll back, flip the flag, shift
            traffic, scale up. Root cause can wait; users cannot.
RESOLVE     Fix the underlying issue properly
LEARN       Blameless post-mortem with concrete, owned action items

Defined severity levels, a named incident commander, a status page for customer communication, and — most importantly — blameless post-mortems. The moment retrospectives assign blame, people stop reporting problems early, and you lose the early warnings that prevent the next outage. The goal is always "what about the system allowed this?" not "who did it?"


PHASE 10 — Security

Security is continuous and layered, not a pre-launch checkbox.

10.1 Defence in Depth

EDGE           WAF, DDoS protection, rate limiting, bot control
NETWORK        VPC isolation, security groups, private subnets, no
               public database endpoints
IDENTITY       Strong authN, MFA, short-lived tokens, RBAC/ABAC
APPLICATION    Input validation, output encoding, parameterised queries,
               CSRF/XSS/SSRF protection, secure headers
DATA           Encryption at rest (KMS) and in transit (TLS 1.2+),
               field-level encryption for sensitive PII, tokenised
               payment data
SECRETS        Secrets Manager / Parameter Store with automatic
               rotation — never in code, config files, images, or
               environment files committed to git
AUDIT          CloudTrail everywhere, immutable log archive in a
               separate account, GuardDuty, Security Hub, Config rules

10.2 Identity and Access

  • Least privilege, always. Start from deny, grant narrowly, review regularly.
  • No long-lived IAM users for services. Use IAM roles — roles for ECS tasks, IRSA for EKS pods, instance profiles for EC2.
  • Federated human access via SSO with MFA; no shared credentials.
  • Just-in-time elevation for production access, time-boxed and logged. Standing production admin access is an unnecessary standing risk.
  • Rotate everything automatically — credentials, keys, certificates.

10.3 Security in the Pipeline (Shift Left)

Pre-commit    Secret scanning (a leaked key in git history is
              permanently leaked — assume compromise and rotate)
Commit        SAST on application code
Dependencies  SCA/CVE scanning with automated update PRs
Build         Container image scanning; block HIGH/CRITICAL
IaC           Terraform/CloudFormation policy scanning (public S3
              buckets, open security groups, unencrypted volumes)
Deploy        Image signature verification, admission policies
Runtime       GuardDuty, Inspector, anomaly detection
Periodic      Penetration testing, red-team exercises, access reviews

10.4 Compliance and Privacy

Depending on jurisdiction and industry: GDPR (consent, right to erasure, data residency), PCI-DSS (never store raw card data — tokenise via the payment provider), SOC 2, HIPAA. These are architectural constraints, not paperwork bolted on afterwards. "Right to erasure" in particular is genuinely hard once personal data has been copied into caches, search indexes, event streams, analytics warehouses, and backups — design for deletion from the beginning.


PHASE 11 — Cost Management

Cloud cost is an engineering concern. Uncontrolled AWS bills are one of the most common ways a technically successful product becomes commercially unviable.

11.1 Visibility First

  • Tag everything — environment, service, team, cost-centre — and enforce tagging with Config rules. Untagged resources are unattributable spend that nobody will ever claim.
  • Enable Cost Explorer and budgets with alerts on day one, not after the first shocking invoice.
  • Set anomaly detection to catch runaway spend within hours rather than at month-end.
  • Review spend monthly with the engineers who own the services, not only with finance.

11.2 Common Optimisations

Right-size            Most workloads are provisioned 2–4x larger than
                      needed. Use Compute Optimizer.
Savings Plans / RIs   40–70% savings on predictable baseline load.
                      Cover the baseline, leave the peak on-demand.
Spot instances        Up to 90% off for fault-tolerant work — batch
                      jobs, CI runners, stateless workers
Auto-shutdown         Non-production environments off nights and
                      weekends: roughly 70% saving on those accounts
S3 lifecycle          Transition to Infrequent Access → Glacier by age
Log retention         Unbounded log retention silently becomes a major
                      line item
NAT Gateway           A classic surprise cost — use VPC endpoints for
                      AWS service traffic
Data transfer         Cross-AZ and cross-region egress adds up fast;
                      keep chatty services in the same AZ where possible
Idle resources        Unattached EBS volumes, orphaned load balancers,
                      forgotten dev stacks, old snapshots
Caching               Every cache hit is a database query not billed —
                      caching is a cost optimisation as much as a
                      performance one

The highest-leverage habit: make cost visible to the engineers who create it. Teams that see the cost of their own service optimise it without being asked.


PHASE 12 — Operate and Evolve

12.1 Day-2 Operations

Launch is the beginning, not the end. The ongoing work:

  • On-call rotation — sustainable, fairly compensated, with a real handover process. Burning out the on-call engineer is a reliability risk.
  • Runbooks for every alert, kept current — a stale runbook is worse than none, because it is trusted.
  • Patching cadence — OS, runtime, base images, dependencies. Automate the boring updates so the urgent ones get attention.
  • Regular restore and failover drills.
  • Capacity planning ahead of known events.
  • Deprecation discipline — remove old API versions, dead feature flags, unused services. Systems accumulate; without deliberate removal, complexity only grows.

12.2 Managing Technical Debt

Debt is not inherently bad — it is a loan, useful when taken deliberately and repaid on schedule. It becomes dangerous when it is invisible and unacknowledged.

  • Maintain a visible debt register with impact and estimated cost
  • Allocate a standing capacity budget (commonly 15–20% of each sprint) — debt work that competes with features in every planning meeting always loses
  • Apply the boy-scout rule: leave touched code better than you found it
  • Prioritise debt that slows delivery or threatens reliability over purely aesthetic refactoring

12.3 Continuous Improvement Loop

Measure   →   Learn   →   Change   →   Measure
   │            │            │
   │            │            └── ship, experiment, refactor
   │            └── post-mortems, user research, analytics
   └── DORA metrics, SLOs, business KPIs, cost per transaction

The DORA metrics are the standard benchmark for delivery health:

Metric Elite performance
Deployment frequency On-demand, multiple times per day
Lead time for changes Under one hour, commit to production
Change failure rate Under 15%
Time to restore service Under one hour

These four correlate strongly with organisational performance — and, importantly, speed and stability rise together rather than trading off. Teams that deploy more frequently have fewer failures, because small changes are easier to verify, and easier to reverse.


Reference: AWS Service Selection

Concern Primary choice Alternatives
DNS Route 53
CDN CloudFront
Edge security WAF + Shield
API entry API Gateway (managed features) ALB (simpler, cheaper at volume)
Containers ECS on Fargate EKS (complex platforms), App Runner (simple apps)
Serverless compute Lambda
Relational data Aurora / RDS
Key-value at scale DynamoDB
Cache ElastiCache (Redis/Valkey) DAX for DynamoDB
Search OpenSearch
Object storage S3
Events EventBridge SNS (fan-out), Kinesis (streams)
Queues SQS Amazon MQ (legacy protocols)
Workflows Step Functions
Secrets Secrets Manager Parameter Store (cheaper, simpler)
CI/CD CodePipeline / CodeBuild Jenkins, GitHub Actions, GitLab CI
Artifacts ECR CodeArtifact for packages
IaC Terraform or CDK CloudFormation, Pulumi
Observability CloudWatch + X-Ray Managed Prometheus/Grafana, third-party
Identity (end users) Cognito Auth0, Okta, self-managed
Analytics Athena over S3 Redshift for a full warehouse

Maturity Roadmap — What to Build When

Not everything above belongs in version one. Building it all up front is its own failure mode: the team spends months on platform work and never validates the product.

STAGE 1 — VALIDATE  (0 → first users)
    Modular monolith · one managed database · App Runner or Fargate
    Basic CI/CD · CloudWatch logs and a handful of alarms
    Managed auth · daily backups
    Goal: learn whether anyone wants this. Optimise for speed of change.

STAGE 2 — STABILISE  (real users, real money)
    Multi-AZ everything · read replicas · Redis caching layer
    Staging environment · automated tests as merge gates
    Structured logging · dashboards · real on-call and runbooks
    Blue-green or canary deploys · IaC for all infrastructure
    Goal: stop breaking. Reliability now has a business cost.

STAGE 3 — SCALE  (growth, larger team)
    Extract services where scaling or team boundaries demand it
    Async event backbone · CQRS/read models where justified
    Distributed tracing · SLOs and error budgets
    Multi-account AWS org · progressive delivery with auto-rollback
    Feature flag platform · cost management practice
    Goal: grow without the team or the system seizing up.

STAGE 4 — OPTIMISE  (scale and maturity)
    Multi-region if genuinely required · chaos engineering
    Platform/internal-developer-platform team · self-service tooling
    Advanced cost optimisation · compliance certifications
    Goal: efficiency, resilience, and developer velocity at scale.

Do not skip stages, and do not build stage 3 during stage 1. Most failed engineering efforts are not caused by insufficient architecture — they are caused by architecture built for a scale that never arrived, at the cost of the iteration speed needed to reach it.


The Principles That Actually Carry

Everything above compresses into a short list:

  1. Requirements drive architecture. Get real numbers before drawing boxes.
  2. Start simple; earn complexity. A modular monolith beats premature microservices almost every time.
  3. Build once, promote the same artifact through every environment.
  4. Configuration comes from the environment, never the artifact.
  5. Everything is code — application, infrastructure, pipeline, policy, documentation.
  6. Design for failure, because in distributed systems failure is the normal case.
  7. Instrument first. You cannot fix, scale, or optimise what you cannot see.
  8. Automate the repeatable; reserve human judgement for the genuinely ambiguous.
  9. Security and cost are design constraints, not clean-up phases.
  10. Small, frequent, reversible changes are the foundation of both speed and stability — they are the same discipline, not a trade-off.