You have the problem chosen, the scope bounded, the cost limit set and the repository created. What you do not have yet is a single resource in Google Cloud, and that is deliberate.

This is the lesson people skip most often and the one that is most expensive to skip. Designing before typing is not an academic ritual: it is the rational response to an uncomfortable technical fact about the cloud. There are decisions you take in thirty seconds and pay for over years.

The projectId cannot be changed. Ever. If you create mi-proyecto-test-2 because you were in a hurry, that identifier will appear in every command, in every console URL, in every log, in every line of the bill and in the screenshot you show in an interview. Changing it means recreating everything inside it.

A subnet's CIDR range cannot be shrunk. A bucket's region cannot be changed. Migrating from Firestore to Cloud SQL — or the other way round — means rewriting the entire data layer. The schema of a partitioned BigQuery table lets you add columns but not change the partition key without recreating it.

None of those decisions is hard. All of them are irreversible. And that is why they are taken with a sheet of paper in front of you, not with the console open.

By the end of this lesson you will have the complete blueprint of your system: which logical components exist, which GCP service implements each one and why that one and not the alternative, what everything is called, who can talk to whom, who has permission to do what, where each piece of data lives and for how long, how much it will cost next month, what you are going to measure to know it works, and the important decisions written down in a format that will still make sense six months from now.

Contents

  1. Why you design before typing, and what "enough design" is
  2. From requirements to logical components
  3. From logical components to GCP services
  4. The three diagrams that are worth having
  5. Designing the organizational structure
  6. Naming and labelling conventions
  7. Network design
  8. Identity design
  9. Data model and data lifecycle
  10. Up-front cost estimate
  11. Defining the project's SLOs
  12. Architecture decision records (ADRs)
  13. Reviewing the design before implementing
  14. The complete design of RefugioReserva

  1. Why you design before typing, and what "enough design" is

The cost-of-change argument

The reason to design is not "doing things properly". It is that the cost of changing a decision grows brutally with the moment at which you change it:

Decision Changing it during design Changing it after implementing Changing it in production
Project name 10 seconds Recreate everything Impossible in practice
Subnet range 10 seconds Recreate the network and everything in it Maintenance window
Firestore ↔ Cloud SQL 1 minute Rewrite the data layer (~15 h) Migration with live data
Region 10 seconds Recreate everything Full migration
Cloud Run ↔ GKE 1 minute Rewrite the deployment (~8 h) Migration with traffic
BigQuery partition key 10 seconds Recreate the table and reload Recreate + gaps in the dashboard
Adding a field to a table 10 seconds 20 minutes 20 minutes

The rows at the top are the ones that justify designing. The one at the bottom is the one that justifies not over-designing: adding a field costs the same before as after, so do not lose an hour deciding whether the notas field should exist.

What "enough design" is for a project this size

Design has very rapidly diminishing returns. For a 50-hour project:

Level of design Time What it produces Verdict
None 0 h You start typing You end up redoing 40 %
Enough 5-8 h The nine artefacts in this lesson ✅ The right amount
Excessive 25 h UML diagrams, a specification for every endpoint, a formal threat model You never get to build

The nine artefacts of "enough design", and none of them takes more than a page:

# Artefact Answers Time
1 List of logical components What pieces does it consist of? 30 min
2 Component → service table with justification What implements each piece and why? 1 h
3 Three diagrams (context, components, deployment) How does it all fit together? 1.5 h
4 Project structure and naming convention What is everything called and where does it live? 30 min
5 Addressing plan + flow matrix Who talks to whom? 45 min
6 Identity and role table Who can do what? 45 min
7 Data model + lifecycle What is stored where and until when? 1 h
8 Cost estimate Does it fit within the limit? 45 min
9 SLI/SLO + ADRs How do I know it works, and why did I decide this? 1 h

The rule that decides whether a design is finished: when you can answer, without hesitating and without opening the console, these five questions:

  1. How much will it cost per month, and why?
  2. What happens if the most important component goes down?
  3. Who can read the users' data?
  4. Which service did you choose and which did you reject, and why?
  5. How will you know it is working properly without anybody telling you?

If you hesitate on any of them, that is where the design is missing.

  1. From requirements to logical components

Before naming a single Google service, describe your system in logical components: functional pieces, provider-agnostic. This step looks bureaucratic and it is the one that avoids the most common beginner mistake, which is designing the architecture as a list of products you fancy using.

Six families of components cover practically any project in this module:

flowchart TB
    subgraph Frontal["Front — user-facing exposure"]
        F1["Public entry point<br/>TLS, domain, cache, filtering"]
    end
    subgraph Servicio["Service — the logic"]
        S1["Web application / API"]
        S2["Asynchronous and<br/>scheduled jobs"]
    end
    subgraph Almacen["Storage"]
        A1["Objects<br/>files, images"]
        A2["Operational database<br/>business state"]
    end
    subgraph Datos["Data and analytics"]
        D1["Event transport"]
        D2["Analytical store"]
        D3["Visualisation"]
    end
    subgraph IA["Intelligence"]
        I1["AI model or API"]
    end
    subgraph Op["Operations"]
        O1["Identity and secrets"]
        O2["Observability"]
        O3["Continuous delivery"]
        O4["Infrastructure as code"]
    end

    F1 --> S1
    S1 --> A1
    S1 --> A2
    S1 --> D1
    S2 --> A2
    D1 --> D2
    D2 --> D3
    S1 --> I1
    O1 -.-> S1
    O2 -.-> S1
    O3 -.-> S1
    O4 -.-> F1

How you fill this in for your project. Take your functional requirements from 08-01 and write, for each component, what it does in your domain:

Logical component What it does in my project Mandatory?
Public entry point Yes (RNF-5)
Web application / API Yes (RF-1)
Asynchronous jobs Yes (RF-4)
Object storage Yes (RF-2)
Operational database Yes (RF-3)
Event transport Yes (RF-4)
Analytical store Yes (RF-5)
Visualisation Yes (RF-5)
AI Yes (RF-6)
Identity and secrets Yes (RNF-3, RNF-4)
Observability Yes (RNF-6, RNF-7)
Continuous delivery Yes (RNF-2)
IaC Yes (RNF-1)

If any box comes out forced — "well, I would put Pub/Sub here because something has to go in" — that is a sign that the problem you chose does not exercise that layer naturally. Two legitimate ways out: change the problem slightly so that it needs it, or implement the simplest possible version of the component and document in an ADR that it is deliberately minimal. What is not acceptable is dropping in a complex service for no reason: that scores negatively in block A of the rubric, not positively.

  1. From logical components to GCP services

Now for the translation. For each component, one row with four mandatory columns: service chosen, why, what you rejected and why you rejected it.

That fourth column is 50 % of block A of the rubric. An interviewer does not ask you "did you use Cloud Run?"; they ask you "why Cloud Run and not App Engine?". If you did not write it down the day you decided, you will not remember it.

The compute tree (from 02-07)

flowchart TD
    A["What do I need to run?"] --> B{"Is it request-response<br/>code in a<br/>container?"}
    B -- Yes --> C{"Do I need cluster<br/>control, operators<br/>or a service mesh?"}
    C -- No --> D["✅ Cloud Run<br/>Scales to zero. Recommended by default"]
    C -- Yes --> E{"Justified in writing<br/>and fits the budget?"}
    E -- No --> D
    E -- Yes --> F["GKE Autopilot"]
    B -- No --> G{"Does it react to an event<br/>and is it a single function?"}
    G -- Yes --> H["✅ Cloud Functions<br/>(gen 2 = Cloud Run underneath)"]
    G -- No --> I{"Is it a process<br/>that starts and finishes?"}
    I -- Yes --> J["✅ Cloud Run Jobs<br/>+ Cloud Scheduler"]
    I -- No --> K{"Do I need the operating<br/>system, a GPU or software<br/>that cannot be containerised?"}
    K -- Yes --> L["Compute Engine"]
    K -- No --> D

For 90 % of the projects in this module, the answer is Cloud Run. And the main reason is not technical but economic: it scales to zero. A portfolio project gets zero traffic 99 % of the time, and anything that does not scale to zero eats the budget from 08-01 without giving anything back.

The database tree (from 02-06 and 02-03)

flowchart TD
    A["What is my data like?"] --> B{"Are there relationships,<br/>and do I need transactions<br/>across several entities?"}
    B -- Yes --> C{"Do I need global<br/>scale or >10 TB?"}
    C -- No --> D["✅ Cloud SQL<br/>PostgreSQL / MySQL"]
    C -- Yes --> E["Spanner<br/>⚠️ expensive for a portfolio"]
    B -- No --> F{"Key access,<br/>documents, no<br/>complex queries?"}
    F -- Yes --> G{"Do I want zero cost<br/>at rest?"}
    G -- Yes --> H["✅ Firestore<br/>Really scales to zero"]
    G -- No --> H
    F -- No --> I{"High-volume<br/>time series?"}
    I -- Yes --> J["Bigtable<br/>⚠️ expensive for a portfolio"]
    I -- No --> D

The real dilemma in this module is Cloud SQL versus Firestore, and it has an economic dimension that needs putting on the table:

Cloud SQL (db-f1-micro) Firestore
Cost at rest ~€7-9/month, always on €0 (generous free tier)
Multi-entity transactions ✅ Native and simple ⚠️ Possible, more limited
Queries with JOIN and aggregation ✅ Full SQL ❌ No JOIN
Data model Normalised relational Documents, denormalised
Learning curve if you come from SQL None Medium
Export to BigQuery Federation / dump Native extension to BigQuery
Good fit if… Your domain has integrity rules Your domain is "documents with state"

If your cost limit is €0, the decision is made: Firestore. If you have €10-15 and your domain has genuine referential integrity (like a booking that cannot exceed capacity), Cloud SQL makes it far easier and the decision justifies itself.

The data tree (from 04-01 and 04-04)

flowchart TD
    A["What do I do with the data?"] --> B{"Do I need to react<br/>to a fact as it happens?"}
    B -- Yes --> C["✅ Pub/Sub<br/>topic + subscription"]
    C --> D{"Is the transformation<br/>complex or continuous?"}
    D -- No --> E["✅ Cloud Function<br/>or Cloud Run push"]
    D -- Yes --> F["Dataflow<br/>⚠️ streaming costs 24 h a day"]
    B -- No --> G{"Is a periodic snapshot<br/>of the data enough?"}
    G -- Yes --> H["✅ Cloud Run Job<br/>+ Cloud Scheduler → BigQuery"]
    G -- No --> C
    E --> I["✅ BigQuery<br/>partitioned tables"]
    F --> I
    H --> I
    I --> J["✅ Looker Studio"]

The economic warning: Dataflow in streaming mode keeps workers running 24 hours a day. For a portfolio project that can be €60-100 a month for a flow that processes twelve messages. Pub/Sub + a function does exactly the same thing for €0 at this volume, and demonstrates just as well that you understand the pattern. Save Dataflow for when the transformation genuinely justifies it, and document the reason.

The translation table you must produce

| Logical component | Service chosen | Why | Rejected alternative | Why it is rejected |
| --- | --- | --- | --- | --- |
| Web application | Cloud Run | Scales to zero, standard container, no node management | GKE Autopilot | The cluster costs from minute 1 and I need nothing from Kubernetes |
| Database | ... | ... | ... | ... |

One row per component. Ten to thirteen rows. It is the most quoted document in your final presentation.

  1. The three diagrams that are worth having

A diagram is not decoration: it is a tool for answering a specific question. And the reason most architecture diagrams are useless is that they try to answer every question at once and end up as a tangle of forty boxes.

The rule: one diagram, one level of detail, one question.

Diagram 1 — Context: who uses the system and what does it talk to?

Answers: what is this and who touches it? Your system is a single box. Around it, the actors and the external systems.

flowchart LR
    U1["👤 Hiker<br/>searches and books"]
    U2["👤 Refuge warden<br/>checks the day's bookings"]
    U3["👤 Federation<br/>checks indicators"]
    U4["👤 Developer (me)<br/>deploys and operates"]

    S(("RefugioReserva<br/>Platform on GCP"))

    E1["Domain registrar<br/>delegated DNS"]
    E2["GitHub<br/>source code"]

    U1 -->|HTTPS| S
    U2 -->|HTTPS + login| S
    U3 -->|Shared report| S
    U4 -->|git push| E2
    E2 -->|triggers build| S
    S -.->|resolves| E1

Ten boxes at most. If you have twenty actors, your scope is too big (go back to 08-01).

Diagram 2 — Components: what parts does it consist of and how do they communicate?

Answers: how does a request and a piece of data flow through the inside? Here the GCP services do appear, but grouped by function, not by product.

flowchart TB
    Usuario["👤 User"]

    subgraph Borde["Edge"]
        LB["Global HTTPS load balancer<br/>+ Cloud CDN + Cloud Armor"]
    end

    subgraph Aplicacion["Application"]
        RUN["Cloud Run<br/>refugio-web"]
        JOB["Cloud Run Job<br/>agregado-nocturno"]
        FN["Cloud Function<br/>procesar-evento"]
    end

    subgraph Datos["Operational data"]
        SQL[("Cloud SQL<br/>PostgreSQL")]
        GCS[("Cloud Storage<br/>photos")]
        SEC["Secret Manager"]
    end

    subgraph Analitica["Analytics"]
        PS["Pub/Sub<br/>reservas-eventos"]
        BQ[("BigQuery<br/>refugio_analitica")]
        LS["Looker Studio"]
    end

    subgraph Inteligencia["AI"]
        NL["Natural Language API"]
    end

    Usuario --> LB --> RUN
    RUN --> SQL
    RUN --> GCS
    RUN --> SEC
    RUN --> PS
    PS --> FN --> BQ
    JOB --> SQL
    JOB --> BQ
    RUN --> NL
    NL --> BQ
    BQ --> LS
    LB -.->|static assets| GCS

The five rules of a readable component diagram:

  1. Fifteen boxes maximum. If you need more, make two diagrams.
  2. Arrows follow the direction of the request, not of the data. If you get confused, add the label.
  3. Group by responsibility (edge, application, data, analytics), not by product.
  4. Label the arrows that are not obvious. RUN --> SQL needs no label; LB -.-> GCS does.
  5. No unnecessary crossings. If two lines cross, reorder the boxes.

Diagram 3 — Deployment: where does each thing physically live?

Answers: which project, which region, which network, what is public and what is not? It is the diagram a security reviewer looks at first.

flowchart TB
    subgraph Internet["🌐 Internet"]
        CL["Client"]
    end

    subgraph Org["Organization / billing account"]
        subgraph ProyProd["Project: refugio-prod · europe-west1"]
            subgraph VPCP["VPC refugio-vpc"]
                subgraph SubP["Subnet app 10.20.0.0/24"]
                    CRP["Cloud Run<br/>(VPC egress connector)"]
                end
                PSC["Private services access<br/>10.20.10.0/24"]
                SQLP[("Cloud SQL<br/>private IP · no public IP")]
            end
            LBP["Global HTTPS load balancer<br/>anycast IP"]
            GCSP[("Buckets")]
        end
        subgraph ProyDev["Project: refugio-dev · europe-west1"]
            DEV["Reduced copy<br/>no CDN, no own domain"]
        end
        subgraph ProyDatos["Project: refugio-datos"]
            BQD[("BigQuery")]
        end
    end

    CL -->|HTTPS 443| LBP --> CRP
    CRP --> PSC --> SQLP
    CRP --> GCSP
    CRP -.-> BQD
    DEV -.-> BQD

This diagram must make obvious, at a glance, what is exposed to the internet. In the example: only the load balancer. The database has no public IP, Cloud Run does not accept direct traffic (only from the load balancer), and the data project is not reachable from outside.

  1. Designing the organizational structure

Do I need an organization?

It depends on your account:

Situation What you have What you do
Personal account with Gmail No organization. Standalone projects The projects hang off "No organization". Valid for the module
Account with Workspace or Cloud Identity on your own domain Organization Create folders and take the chance to practise 07-07
Your company's account Somebody else's organization Do not use it. Create a personal account for the project

Without an organization you lose organization policies and folders, but everything else works the same and the rubric does not require an organization. If you have your own domain (which you are going to need anyway for RNF-5), you can create a free Cloud Identity and have an organization; it is a bonus that adds points but is not mandatory.

How many projects?

A project is the unit of isolation, of billing and of IAM. Three reasonable options:

Option Projects Advantages Drawbacks Recommended if
A. Just one miproy Simple, cheap, fast You do not demonstrate environment separation. One mistake affects everything Your limit is €0 and you are very short of time
B. Two (dev + prod) miproy-dev, miproy-prod The right balance. You demonstrate real promotion between environments You duplicate some resources ✅ By default
C. Four or more -dev, -prod, -datos, -cicd Resembles AlpinaShop. Separates analytics and delivery More IAM to manage, more time If you have time to spare

The recommendation is B, with a cheap variant: if your analytics are small, put BigQuery in the production project and save yourself the fourth project. What is always worth separating is dev from prod, because that is what makes the promotion pipeline of RNF-2 credible and because it lets you destroy dev on Fridays without fear.

flowchart TB
    ORG["Organization (optional)<br/>tudominio.example"]
    ORG --> P1["refugio-dev<br/>Development environment<br/>Destroyable"]
    ORG --> P2["refugio-prod<br/>Production environment<br/>What you show"]
    ORG --> P3["refugio-datos<br/>BigQuery + Looker<br/>(optional)"]
    ORG --> P4["refugio-cicd<br/>Artifact Registry + builds<br/>(optional)"]

    P4 -.->|deploys into| P1
    P4 -.->|with approval| P2
    P1 -.->|writes to dev dataset| P3
    P2 -.->|writes to prod dataset| P3

The region decision

A single region. For a project in Spain, europe-west1 (Belgium) or europe-southwest1 (Madrid).

Criterion europe-west1 europe-southwest1
Latency from Spain ~25-35 ms ~5-15 ms
Service availability Practically everything Very good, some services arrive later
Price Reference Slightly higher on some services
Data on Spanish territory No (Belgium, still the EU) Yes

Write the decision and its reason. It is a three-line ADR, and it is the kind of detail that shows you think about data residency and not just about making it work. And remember from 01-05: some services are global (IAM, Cloud DNS, the global load balancer, Artifact Registry in multi-region mode), so the region does not apply to everything.

  1. Naming and labelling conventions

Decided once, applied always, and decided now because the projectId cannot be changed.

The pattern

<project>-<environment>                    for projectId
<project>-<component>[-<detail>]           for resources inside the project
Resource Pattern Good example Bad example
Project <proj>-<env> refugio-prod mi-proyecto-final-2
VPC <proj>-vpc refugio-vpc default
Subnet <proj>-<use>-<region> refugio-app-ew1 subnet-1
Cloud Run service <proj>-<function> refugio-web service
Cloud SQL <proj>-db refugio-db instance-1
Bucket <proj>-<use>-<suffix> refugio-fotos-8f2a mis-fotos
Service account sa-<workload> sa-refugio-web service-account-1
BigQuery dataset <proj>_<domain> refugio_analitica dataset1
Pub/Sub topic <entity>-<fact> reservas-creadas topic1
Secret <proj>-<what> refugio-db-password password

The hard rules GCP imposes and that you need to know beforehand:

  • projectId: 6-30 characters, lowercase letters, digits and hyphens; starts with a letter; unique across all of Google Cloud and never reusable, not even after deleting the project.
  • Buckets: the name is globally unique. That is why they carry a random suffix: refugio-fotos will almost certainly be taken. Terraform solves it with random_id.
  • Service accounts: the accountId (the part before the @) is 6-30 characters and also cannot be changed.
  • No underscores in DNS-compatible names (buckets, Cloud Run services). They are fine in BigQuery datasets and tables, which use _.
  • No names containing test, tmp, 2, nuevo, final. All of that ends up in production and stays there.

Labels

Labels are key-value pairs that appear in the billing export. They are what lets you answer "how much does the data component cost me?" without guessing. They cost nothing and go in from the very first apply.

Label Values What for
proyecto refugioreserva Distinguishing it from other things in the same account
entorno dev, prod Attributing cost per environment
componente web, datos, analitica, ia, red Attributing cost per layer
gestionado-por terraform, manual Detecting anything created by hand
caduca 2026-12-31 Temporary resources that need cleaning up

In Terraform they are applied in one go with a shared variable:

locals {
  etiquetas_base = {
    proyecto       = "refugioreserva"
    entorno        = var.entorno
    gestionado-por = "terraform"
  }
}

resource "google_storage_bucket" "fotos" {
  name     = "refugio-fotos-${var.sufijo}"
  location = var.region
  labels   = merge(local.etiquetas_base, { componente = "web" })
}

gestionado-por = terraform is the most useful of the five: any resource without that label, or created by hand, stands out in a billing query. It is your zero-cost configuration drift detector.

  1. Network design

Even if you use Cloud Run — which is serverless — there are network decisions to take, because the private database, the egress connector and the load balancer live in a VPC.

Addressing plan

Do not use the default network. It comes with subnets in every region in the world and permissive firewall rules you did not choose. Create a VPC in custom mode.

Reserve a space and divide it up. A private /16 is more than enough:

Block CIDR Use Notes
Project space 10.20.0.0/16 Everything Does not overlap with your home network (192.168.x) or the typical office one (10.0.x)
Application subnet 10.20.0.0/24 VMs, GKE if there were any 254 addresses, plenty
VPC egress connector 10.20.8.0/28 Serverless connector Requires an exact /28
Private services access 10.20.16.0/20 Cloud SQL with private IP Google requires a reserved block, minimum /24; /20 is recommended
Reserved for the future 10.20.32.0/19 Unused Always leave free space

The two traps that catch everybody the first time:

  1. The Serverless VPC Access connector needs its own non-overlapping /28. Not a /29, not a /27: a /28.
  2. Cloud SQL with a private IP needs a range reserved for private services access and a peering with Google's network. It is an extra step that the documentation mentions in passing and that produces a cryptic error if it is missing. In Terraform it is two resources: google_compute_global_address with purpose = "VPC_PEERING" and google_service_networking_connection.

The allowed flow matrix

This is the most valuable artefact in the network design: a table of who can talk to whom. It is written before creating a single firewall rule, and afterwards it translates into rules almost mechanically.

Source Destination Port Protocol Allowed? Reason
Internet Load balancer 443 TCP ✅ It is the entry point
Internet Load balancer 80 TCP ✅ Only to redirect to 443
Internet Cloud Run directly — — ❌ It must go through the load balancer (--ingress=internal-and-cloud-load-balancing)
Internet Cloud SQL 5432 TCP ❌ Never. No public IP
Load balancer Cloud Run 443 TCP ✅ Serverless NEG
Cloud Run Cloud SQL 5432 TCP ✅ Over private IP, via the connector
Cloud Run Cloud Storage 443 TCP ✅ Google API
Cloud Run Secret Manager 443 TCP ✅ Google API
Cloud Run Pub/Sub 443 TCP ✅ Google API
Cloud Run Open internet — — ⚠️ Only if I call an external API. Otherwise it is closed
Cloud Function BigQuery 443 TCP ✅ Google API
Me (home IP) Cloud SQL 5432 TCP ⚠️ Only through an authenticated proxy, never a public IP
Anyone Anything — — ❌ Deny by default

The last row is the policy: anything not explicitly allowed is denied. It is the principle from 03-01 and it is the difference between a network that was designed and a network that "works".

Minimum public exposure

Write the list of everything that will be reachable from the internet. It should fit in three lines:

## Public surface of RefugioReserva
1. `https://refugioreserva.example` → load balancer → Cloud Run (the whole app).
2. `https://refugioreserva.example/fotos/*` → bucket via CDN (public thumbnails only).
3. Nothing else. The DB has no public IP. Cloud Run does not accept direct traffic.
   The originals bucket and the Terraform state bucket are private.

If that list has more than five entries, review it: almost certainly something is exposed that does not need to be.

  1. Identity design

Same as with the network: you write the table before creating anything. And you apply the principle from 03-04 in its most practical form: one service account per workload, with the minimum roles, and no downloaded keys.

The three identity types in your project

Type Who it is How it authenticates
Humans You, and whoever reviews your project Your Google account, with 2FA
Workloads Cloud Run, functions, jobs Attached service account, no key
External CI/CD GitHub Actions or Cloud Build Workload Identity Federation, no key

The identity table

Identity Type Roles On which resource Why exactly this
[email protected] Human roles/owner Projects You are the administrator. It is the only acceptable owner
sa-refugio-web Cloud Run (app) roles/cloudsql.client Project Connect to the DB, not administer it
roles/secretmanager.secretAccessor Only the 2 secrets it uses Read, not list or create
roles/storage.objectAdmin Only the photos bucket Upload and read photos
roles/pubsub.publisher Only the events topic Publish, not subscribe
roles/logging.logWriter Project Write logs
roles/cloudtrace.agent Project Send traces
sa-procesar-evento Cloud Function roles/bigquery.dataEditor Only the analytical dataset Insert rows
roles/language.user (per the API) Project Call the NL API
sa-job-nocturno Cloud Run Job roles/cloudsql.client Project Read the DB
roles/bigquery.jobUser + dataEditor Dataset Load aggregates
sa-deploy CI/CD (WIF) roles/run.developer Project Deploy revisions
roles/artifactregistry.writer Repository Publish images
roles/iam.serviceAccountUser Only on sa-refugio-web Being able to attach that SA to the service

The five principles you must respect and that the rubric checks:

  1. Never roles/editor or roles/owner on a service account. They are the primitive roles from 03-04 and they grant thousands of permissions.
  2. The narrowest possible scope. secretAccessor on the secret, not on the project. In Terraform that is google_secret_manager_secret_iam_member, not google_project_iam_member.
  3. One SA per workload. If the function and the app share an SA, they share permissions, and the blast radius of a failure doubles.
  4. Zero JSON keys. Workloads inside GCP use the attached SA; external CI/CD uses WIF. If you have a .json, you have failed.
  5. serviceAccountUser is the permission everybody forgets. For sa-deploy to be able to deploy a service that runs as sa-refugio-web, it needs to act as that account. It is the most frequent 403 of the first automated deployment.

And the one that gets discovered late: Compute Engine's default service account has the editor role. If you let your resources use it, your whole least-privilege design is decorative. Create explicit SAs for everything.

  1. Data model and data lifecycle

What is stored where, and why

The table that avoids the most expensive mistake in these projects, which is putting everything into the operational database:

Data Where it lives Why there What does NOT go here
Business state (bookings, users, catalogue) Cloud SQL It needs transactions and integrity Event history, files
Binary files (images, PDFs) Cloud Storage Cheap, servable by CDN, does not bloat the DB Anything structured you need to query
Business events (immutable history) BigQuery Analytics, aggregation, cost per query Data you need to read in <100 ms
Sessions and ephemeral state Firestore or memory Key access, TTL Anything that must survive
Passwords, keys, connection strings Secret Manager Encrypted, versioned, audited Never in plain environment variables or in the repo
Non-sensitive configuration Service environment variables Changes without rebuilding the image Secrets
Terraform state Dedicated bucket with versioning Locking and recovery Anything else

The operational schema

Write it as DDL, even if it is a draft. It is what forces you to think about keys, constraints and types:

-- Operational schema of RefugioReserva (Cloud SQL PostgreSQL)
-- All data is FICTIONAL.

CREATE TABLE refugio (
    id            SERIAL PRIMARY KEY,
    nombre        TEXT        NOT NULL UNIQUE,
    altitud_m     INTEGER     NOT NULL CHECK (altitud_m BETWEEN 500 AND 3500),
    capacidad     INTEGER     NOT NULL CHECK (capacidad > 0),
    activo        BOOLEAN     NOT NULL DEFAULT TRUE,
    creado_en     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE reserva (
    id            UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    refugio_id    INTEGER     NOT NULL REFERENCES refugio(id),
    fecha         DATE        NOT NULL,
    plazas        INTEGER     NOT NULL CHECK (plazas BETWEEN 1 AND 12),
    -- Contact details: FICTIONAL. See the retention policy further down.
    nombre_titular TEXT       NOT NULL,
    email_titular  TEXT       NOT NULL,
    estado        TEXT        NOT NULL DEFAULT 'confirmada'
                              CHECK (estado IN ('confirmada','cancelada')),
    creada_en     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Index that supports the most frequent query: availability by refuge and date
CREATE INDEX idx_reserva_refugio_fecha ON reserva (refugio_id, fecha)
    WHERE estado = 'confirmada';

CREATE TABLE opinion (
    id            SERIAL PRIMARY KEY,
    refugio_id    INTEGER     NOT NULL REFERENCES refugio(id),
    texto         TEXT        NOT NULL,
    puntuacion    INTEGER     CHECK (puntuacion BETWEEN 1 AND 5),
    -- Filled in by the Natural Language API
    sentimiento   NUMERIC(4,3),
    magnitud      NUMERIC(4,3),
    creada_en     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Three details that deserve attention and that score in the rubric:

  • The CHECK on plazas is a business rule expressed in the database. It is free and it prevents impossible data even if the application has a bug.
  • The partial index (WHERE estado = 'confirmada') is smaller and faster than a full one, because cancelled bookings do not take part in the availability calculation.
  • The capacity check is not in the schema. It cannot be: it depends on the sum of bookings for a given date. It goes in a transaction with a lock in the application, and it is precisely what the mandatory integration test will test.

The analytical schema

In BigQuery the criteria are different: you denormalise, you partition by date and you cluster by whatever you filter on most. The reason is economic: BigQuery charges by bytes read, and an unpartitioned table is read in full every time.

-- Analytical store. Partitioned by day and clustered by refuge.
CREATE TABLE `refugio-datos.refugio_analitica.reservas_eventos` (
  evento_id       STRING    NOT NULL,
  ocurrido_en     TIMESTAMP NOT NULL,
  tipo_evento     STRING    NOT NULL,   -- created | cancelled
  reserva_id      STRING    NOT NULL,
  refugio_id      INT64     NOT NULL,
  refugio_nombre  STRING,               -- denormalised on purpose
  fecha_estancia  DATE      NOT NULL,
  plazas          INT64     NOT NULL,
  -- NO contact details: they are not replicated to the analytical store
)
PARTITION BY DATE(ocurrido_en)
CLUSTER BY refugio_id
OPTIONS (
  partition_expiration_days = 1095,      -- 3 years, then deleted automatically
  require_partition_filter  = TRUE       -- forces filtering by date
);

require_partition_filter = TRUE is the option that saves your bill. With it, a query without a WHERE DATE(ocurrido_en) ... fails instead of scanning the whole table. It is the equivalent of a seatbelt and in a project with a cost limit it is not optional.

Notice too what is not there: contact details do not travel to the analytical store. Nobody needs the booker's email to calculate occupancy, and every piece of personal data you do not replicate is a GDPR problem you do not have.

The data lifecycle

Every piece of data is born, lives and must die. The last part is the one that never gets designed.

Data Born Lives Dies Mechanism
Booking On booking In Cloud SQL 24 months after the stay Nightly job that anonymises contact details
Contact details With the booking In Cloud SQL 6 months after the stay UPDATE that replaces them with anonimizado
Original photo On upload Standard bucket Never (they belong to the refuge) —
Thumbnail Generated Bucket + CDN Regenerated if lost Derived, not critical
Analytical event On booking BigQuery 3 years partition_expiration_days
Application log Every request Cloud Logging 30 days Log bucket retention
Cloud SQL backup Daily 7 days Automatic Backup retention
Container image Every build Artifact Registry 10 versions Cleanup policy

⚠️ GDPR warning, and this is not a formality. As soon as your system stores a name, an email, a phone number or a position associated with an identifiable person, you are processing personal data, even if it is made up and even if it is a learning project.

For this project: use fictional data exclusively (generated names, @example.com emails, phone numbers from the range reserved for fiction). With that, formally there is no processing of personal data and no obligation.

But design as if there were, because the habit is what is being assessed and what transfers to a real job: minimisation (do not store what you do not use), purpose limitation (do not replicate contact details to the analytical store), a defined retention period with real, automatic deletion, encryption at rest, restricted access and traceability of who accesses what.

And if this design were ever going to process real people's data, a full GDPR and compliance review would be needed — legal basis, record of processing activities, impact assessment where applicable, data processing agreement with Google — which is entirely outside the scope of this course.

  1. Up-front cost estimate

It is done now, with the design in hand and before building. If it does not fit within the limit from 08-01, what changes is the design, not the limit.

A five-step method

  1. List the services from your translation table.
  2. For each one, estimate the driver that determines the price: hours running, GB stored, GB processed, number of requests, GB of egress to the internet.
  3. Check whether the free tier covers it. Surprisingly often, it does.
  4. Use the official calculator for whatever it does not cover.
  5. Add it up and add 25 % of headroom for what you have not foreseen.

The drivers that matter

Service What you pay for How you estimate it Common trap
Cloud Run vCPU-s + GiB-s + requests Requests/month × average duration min-instances > 0 bills 24 h a day
Cloud SQL Instance running, per hour 730 h × price/h You pay even when you do not use it
Cloud Storage GB/month + operations + egress Estimated GB Egress to the internet is the expensive part, not the storage
BigQuery Bytes read + GB stored Queries/day × bytes per query A dashboard that auto-refreshes scans non-stop
Pub/Sub GiB published Messages × size Negligible volume in this project
Cloud Functions Invocations + GB-s Invocations/month Same
HTTP(S) load balancer Forwarding rule per hour + traffic 730 h × price It costs money even with no traffic
Cloud Logging GB ingested above the free allowance Log volume Debug logs in production
Artifact Registry GB stored Images × size Nobody deletes old images
AI APIs Per unit processed Documents/images per month Reprocessing on every page load

The four money sinks of a portfolio project, in order of frequency:

  1. Cloud SQL running 24/7. It is almost always the biggest line item.
  2. The global load balancer. The forwarding rule costs per hour whether or not there is traffic. It can be €15-20/month. If your limit is very tight, consider serving Cloud Run with its own custom domain (which gives free TLS) and documenting in an ADR that you give up CDN and Cloud Armor on cost grounds.
  3. Standard GKE. A three-node cluster is around €100/month. Do not use it.
  4. Streaming Dataflow. Workers running permanently.

The estimate table

| Service | Configuration | Estimated driver | Free tier | Cost/month |
| --- | --- | --- | --- | --- |
| Cloud Run web | 1 vCPU / 512 MiB / min=0 | 20,000 req., 200 ms | Yes, covers almost all of it | ~€0.20 |
| Cloud SQL | db-f1-micro, 10 GB HDD | 730 h | No | ~€8.00 |
| ... | | | | |
| **Subtotal** | | | | **€X** |
| **+25 % headroom** | | | | **€Y** |
| **Limit set (08-01)** | | | | **€12** |
| **Does it fit?** | | | | ✅ / ❌ |

What to do if it does not fit

In this order, from the cheapest change to the most painful:

# Measure Typical saving Cost to the project
1 Switch off the development environment when not in use (destroy/apply) 40-50 % of the total None, if your IaC works
2 Drop Cloud SQL to the smallest instance and no HA 50-70 % of that line item None in a portfolio
3 Schedule an overnight shutdown of Cloud SQL (9:00-23:00) ~40 % more The demo does not work overnight
4 Reduce log retention to 7 days Small Less history for diagnosis
5 Drop CDN and load balancer; use Cloud Run's custom domain €15-20/month You lose Cloud Armor and caching. Requires an ADR
6 Swap Cloud SQL for Firestore The entire line item Redesign of the data layer. Requires an ADR
7 Reduce functional scope Variable Last resort

And one decision that has to be taken consciously: if your limit is €0, the correct architecture is Firestore + Cloud Run + BigQuery + Cloud Functions + a Cloud Run custom domain, with no load balancer and no Cloud SQL. It is a perfectly defensible design, as long as why is written down. A project that says "I chose Firestore because my cost constraint was €0 and Cloud SQL costs €8/month running" demonstrates more judgement than one that spent €60 without thinking about it.

  1. Defining the project's SLOs

From 07-06, in its minimum viable version. One or two SLOs are enough. The mistake is defining six and measuring none.

The process in four steps

Step 1 — What is the critical user journey? Not "the site works". Something concrete: "a hiker checks availability and completes a booking".

Step 2 — Which SLI measures it? An SLI is a ratio of good events to valid events:

SLI type Formula When to use it
Availability requests with code < 500 ÷ total requests Always. It is the basic one
Latency requests < threshold ÷ total requests If perception matters
Correctness correct operations ÷ attempted operations For flows involving money or state
Freshness data with lag < X ÷ total For data pipelines

Step 3 — Set the target. Be realistic: the target must be achievable with your architecture and higher than what a user notices. 99.9 % monthly is a good starting point; 99.99 % in a portfolio project with a single region is a lie, because the region itself does not guarantee it.

Step 4 — Calculate the error budget. It is what makes the SLO useful:

SLO Allowed error/month In time (30 days)
99 % 1 % 7 h 12 min
99.5 % 0.5 % 3 h 36 min
99.9 % 0.1 % 43 min 12 s
99.95 % 0.05 % 21 min 36 s
99.99 % 0.01 % 4 min 19 s

The project's SLO table

# Critical journey SLI Target Window Error budget Measurement source
SLO-1 Check availability % of requests to /api/disponibilidad with code < 500 99.5 % 30 days 3 h 36 min run.googleapis.com/request_count metric
SLO-2 Complete a booking % of POST /api/reservas with latency < 1,500 ms 95 % 30 days 36 h of slow requests request_latencies distribution

And the error budget policy, written now, which is what turns the SLO into a tool rather than a decorative number:

## Error budget policy (RefugioReserva)
- **Budget > 50 % remaining:** normal development. Deploy whenever you like.
- **Budget < 50 %:** every production deployment requires the end-to-end
  test to have passed in dev.
- **Budget < 10 %:** new functionality is frozen. Reliability fixes only
  until it recovers.
- **Budget exhausted:** a short post-mortem is written in `docs/diario.md`
  before deploying anything again.

In a one-person project this may sound excessive. It is not: it is one paragraph, and it is exactly the kind of artefact that in an interview shows you have understood what an SLO is for.

  1. Architecture decision records (ADRs)

An ADR (Architecture Decision Record) is one page that records one important decision. It is the most valuable document you will write in this project, and here is why, without embellishment:

  • Three months from now you will not remember why you chose Firestore. The code says what you did; the ADR says why.
  • In an interview they are going to ask you exactly that. An ADR is a prepared answer.
  • Writing the decision down forces you to actually have one. Half the time, when you reach the "alternatives" section you discover you had not considered any.
  • It lets you revisit the decision honestly. If the context changes, you write a new ADR that supersedes the previous one. You do not rewrite the old one: you leave it as a historical record. That is how the evolution of your judgement becomes visible, which is what is being assessed.

The template

# ADR-00X: [Title phrased as a decision, not as a question]

- **Date:** 2026-09-12
- **Status:** Proposed | **Accepted** | Superseded by ADR-00Y | Obsolete

## Context
What situation forces a decision? What constraints are there (cost, time,
knowledge, requirements)? Facts, not opinions. 5-10 lines.

## Options considered
### Option A — <name>
- Advantages:
- Drawbacks:
- Estimated cost:

### Option B — <name>
- Advantages:
- Drawbacks:
- Estimated cost:

## Decision
We choose **<option>**.
Because <the criterion that decided it>. The deciding factor was <just one>.

## Consequences
### Positive
-
### Negative (what we accept losing)
-
### What would change this decision
If <X> happens, it would need revisiting.

The two sections people leave weak and that are the ones that count:

  • "Negative": every architecture decision loses something. If your ADR has no drawbacks, you have not decided: you have justified what you already wanted to do. Writing "I accept that without a load balancer I lose Cloud Armor and edge caching" demonstrates judgement, not weakness.
  • "What would change this decision": it turns the decision into something revisable and shows you know it is contextual. It is exactly what AlpinaShop did with DA-004 on Anthos.

The ADRs you need as a minimum

ADR Decision Why it matters
ADR-001 Compute service It is the application's structural decision
ADR-002 Database It is the most expensive to reverse
ADR-003 Data strategy (streaming vs. batch) It determines complexity and cost
ADR-004 Approach for the AI component Pre-trained API vs. your own model
ADR-005 Project structure and region Irreversible
ADR-006 (common) Cost cuts The most honest one and the one that impresses most

Three is the rubric's minimum; five or six is normal. One per design session and you have them.

  1. Reviewing the design before implementing

This is milestone M1 from 08-01. You either pass it or you do not; there is no "almost". Go through it yourself, in the cold light of day, ideally the day after finishing the design.

Design checklist

Architecture

  • [ ] All six functional requirements have a service assigned
  • [ ] All eight non-functional requirements have a mechanism assigned
  • [ ] Every service has its justification and its rejected alternative written down
  • [ ] The three diagrams exist and are consistent with each other
  • [ ] No diagram has more than 15 boxes

Organization and names

  • [ ] It is decided how many projects there are and exactly what they are called
  • [ ] The projectIds comply with the rules (6-30, lowercase, no test/final/2)
  • [ ] The region is decided and the reason is written down
  • [ ] There is a naming convention for every resource type
  • [ ] There is a labelling convention, including gestionado-por

Network

  • [ ] There is an addressing plan with no overlaps
  • [ ] The serverless connector has its /28
  • [ ] Private Cloud SQL has its private services access range
  • [ ] The flow matrix is written, with deny by default
  • [ ] The public surface fits in five lines

Identity

  • [ ] There is one service account per workload
  • [ ] None of them has owner or editor
  • [ ] Every role is at the narrowest possible scope
  • [ ] WIF is planned for CI/CD, with no JSON keys
  • [ ] serviceAccountUser is accounted for the deployer

Data

  • [ ] It is clear which data lives where and why
  • [ ] The operational schema is written as DDL
  • [ ] The analytical table is partitioned, with require_partition_filter
  • [ ] The lifecycle of every piece of data is defined, including its deletion
  • [ ] No contact data is replicated to the analytical store
  • [ ] It is noted that all the data is fictional

Cost

  • [ ] There is an estimate table by service
  • [ ] The total with 25 % headroom fits within the limit from 08-01
  • [ ] If it did not fit, there is an ADR documenting the cut
  • [ ] The budget with alerts is already created

Operations

  • [ ] There is at least one SLI and one SLO with its window
  • [ ] The error budget is calculated
  • [ ] There is a written error budget policy
  • [ ] It is planned what is going to be monitored

Documentation

  • [ ] There are ≥3 ADRs with context, options, decision and consequences
  • [ ] docs/arquitectura.md is written and contains the diagrams
  • [ ] The diario.md has entries for every design session

The five control questions

If you have ticked every box and still cannot answer these five without looking, the design is not finished:

  1. How much will it cost per month and what is the biggest line item?
  2. If the database goes down, what stops working and what keeps working?
  3. Who — which identity exactly — can read the users' data?
  4. What did you reject in the most important decision, and why?
  5. How will you find out something is wrong before somebody tells you?

  1. The complete design of RefugioReserva

As a consolidated reference, this is how the example project's design ends up. Read it as a model of format and level of detail, not as something to copy.

Translation table

Logical component Service Why Rejected Why it is rejected
Entry point Global HTTPS load balancer + CDN + Cloud Armor Managed certificate, photo caching, basic WAF Cloud Run custom domain Cheaper, but no CDN and no WAF. It fits the budget, so the load balancer stays
Application Cloud Run refugio-web Scales to zero (almost no traffic), standard container GKE Autopilot Cost from minute 1, I need nothing from Kubernetes
App Engine standard Less portable, and I want the container for RF-1
Scheduled job Cloud Run Job + Scheduler Reuses the same image Cloud Composer Disproportionate: a DAG for one task
Objects Cloud Storage refugio-fotos Cheap, integrated with the CDN Storing them in the DB Bloats the DB and makes backups more expensive
Operational DB Cloud SQL PostgreSQL db-f1-micro Capacity integrity, transaction with a lock Firestore No convenient multi-document transactions for capacity control. Cost: +€8/month, accepted
Events Pub/Sub reservas-eventos Decouples the web from analytics Writing to BigQuery from the web Couples user latency to BigQuery
Processing Cloud Function procesar-evento Minimal volume, zero cost Streaming Dataflow ~€70/month for 60 messages a day
Analytical store BigQuery refugio_analitica Free tier is enough, SQL Querying the Cloud SQL replica Mixes analytical and operational load
Visualisation Looker Studio Free, native connector Grafana It would have to be hosted and paid for
AI Natural Language API (sentiment) Works on day one, no training Text AutoML Days of work and training cost for the same requirement
Identity 4 service accounts + WIF Least privilege, no keys One SA for everything Blast radius, and it fails block D
Secrets Secret Manager Versioned and audited Environment variables They stay visible in the console and in describe
Observability Cloud Monitoring + Logging + one SLO Native, no cost at this volume Self-managed Prometheus Cost and time
Delivery Cloud Build from GitHub Native WIF with GCP GitHub Actions Equivalent; Cloud Build is chosen for its Artifact Registry integration
IaC Terraform, GCS backend Industry standard, portable Deployment Manager Being retired since 2025 (see 06-05)

Structure and names

Element Value
Projects refugio-dev, refugio-prod, refugio-datos
Region europe-west1 (Belgium). Reason: full service catalogue and reference pricing; the extra 20 ms of latency is irrelevant for this use case
VPC refugio-vpc (custom, one per project)
IP space dev 10.10.0.0/16 · prod 10.20.0.0/16
Labels proyecto=refugioreserva, entorno, componente, gestionado-por
Domain refugioreserva.example (prod), dev.refugioreserva.example (dev)

Estimated cost

Service Configuration Cost/month
Cloud Run (prod + dev) 1 vCPU, 512 MiB, min=0 ~€0.30
Cloud SQL prod db-f1-micro, 10 GB HDD, no HA ~€8.00
Cloud SQL dev Created and destroyed per session (~20 h/month) ~€0.25
Cloud Storage 2 GB Standard + lifecycle ~€0.10
BigQuery <1 GB, <5 GB queried €0.00
Pub/Sub + Functions ~2,000 events/month €0.00
HTTPS load balancer 1 forwarding rule ~€18.00 ❌
Artifact Registry 2 GB with cleanup at 10 versions ~€0.20
NL API 400 documents, one time only ~€0.40
Subtotal €27.25
+25 % headroom €34.06
Limit (08-01) €12.00
Does it fit? ❌ No

It does not fit. The load balancer eats more than everything else put together. This is not a design failure: it is exactly what estimating before building is for. And the answer goes into an ADR:

# ADR-006: Give up the global load balancer and use Cloud Run's custom domain

- **Date:** 2026-09-13
- **Status:** Accepted

## Context
The initial design uses a global HTTPS load balancer with Cloud CDN and Cloud Armor.
The cost estimate comes to €34/month against a self-imposed limit of €12.
The load balancer alone is ~€18/month, and it charges for existing, not for traffic.
The project's expected real traffic is tens of requests a day.

## Options considered
### A. Keep the load balancer and raise the limit to €40/month
- Advantages: complete architecture, CDN and WAF, practice with 03-02/03-03/03-05.
- Drawbacks: 3.3 times the limit. It nullifies the constraint exercise.

### B. Cloud Run custom domain (managed TLS included)
- Advantages: €0. Valid TLS. Meets RNF-5.
- Drawbacks: no Cloud CDN, no Cloud Armor, no traffic distribution
  across heterogeneous backends.

### C. Load balancer for one week only, to demonstrate it, then destroy it
- Advantages: cost ~€4. I demonstrate that I know how to set it up, with
  screenshots and with the Terraform written.
- Drawbacks: the final architecture does not have it; it has to be explained.

## Decision
We choose **C**. The `infra/modules/balanceador/` module stays written and tested;
it is deployed during the load-testing week (08-04), documented with
screenshots and cache measurements, and destroyed afterwards. The project's
permanent state uses Cloud Run's custom domain.

The deciding factor: the cost constraint is a project requirement,
not an obstacle, and skipping it would mean failing the exercise itself.

## Consequences
### Positive
- Stable cost of ~€9/month, within the limit, with headroom.
- The load balancer code exists and is tested: the knowledge stays.
- It gives material for the presentation: a real, measured cost decision.

### Negative
- The permanent architecture has no WAF. It is partly mitigated with
  concurrency limits in Cloud Run and input validation in the app.
- Without a CDN, photos are served from the bucket with signed URLs; worse
  latency, irrelevant at this volume.

## What would change this decision
If the project were to receive real traffic (>10,000 visits/month) or if the limit
rose above €30/month, the load balancer would be redeployed: the Terraform
module is already written and it would be one `apply`.

Revised cost: €9.55/month, within the limit. And, above all: a story to tell in the final presentation that is worth more than the load balancer.

SLOs

# SLI Target Window Error budget
SLO-1 % of requests to /api/* with code < 500 99.5 % 30 days 3 h 36 min
SLO-2 % of POST /api/reservas with latency < 1,500 ms 95 % 30 days —

Common Mistakes and Tips

Designing the architecture as a list of products you want to use. The symptom is a diagram with twelve GCP logos and no justification. The cure is the logical-components step: first what is needed, then what implements it.

Skipping the "rejected alternative" column. It is half of the 20 points in block A. And it is the literal question in an interview.

Creating the projects with provisional names. There are no provisional names in GCP: the projectId is forever and cannot be reused, not even after deleting it.

Using the default VPC. It brings subnets in every region and permissive rules you did not choose. Creating a custom VPC costs five lines of Terraform.

Forgetting the private services access range. Cloud SQL with a private IP needs a reserved block and a peering. If it is not in the design, it shows up as an incomprehensible error the day you create the database.

Not partitioning your BigQuery tables. It is the mistake you pay for in the bill, not in performance. And require_partition_filter = TRUE is your safety net.

Defining six SLOs. One well-measured SLO is worth more than six in a document. Start with availability.

Designing the lifecycle without the deletion. "It is kept forever" is not a retention policy: it is the absence of one. And as soon as there is real personal data, it is a breach.

Tip: write the ADR the day you take the decision, not at the end. Fifteen minutes then, or an invented reconstruction three weeks later.

Tip: draw the diagrams in mermaid inside the repository, not in an external tool. They get versioned with the code, they are updated in the same commit as the change and they render in GitHub. A diagram in a PNG exported from a web tool is out of date from day two.

Tip: do the cost estimate even if you are sure it fits. The RefugioReserva case — where the load balancer was eating three times the whole budget — is the norm, not the exception. That discovery is worth more than the design itself.

Tip: if you hesitate between two services for more than twenty minutes, choose the simpler one and write the ADR. A reversible decision taken quickly beats a perfect decision taken late. And the ADR already leaves the door open.

Exercises

Exercise 1 — Translate your requirements into architecture and draw it

Produce, for your project: the list of logical components with what each one does in your domain; the complete translation table with the four mandatory columns (service, why, rejected alternative, why it is rejected); and the three mermaid diagrams — context, components and deployment — respecting the maximum of 15 boxes and the five readability rules.

Save it all in docs/arquitectura.md.

Exercise 2 — Design network, identity and data on paper

Without creating anything in GCP, produce: the addressing plan with its blocks (including the connector's /28 and the private services access range); the allowed flow matrix with the deny-by-default row; the public surface list; the identity table with one service account per workload and its roles at the narrowest scope; the DDL of the operational schema; the schema of the partitioned analytical table; and the lifecycle table for every piece of data with its deletion mechanism.

Add the explicit note that all the data is fictional and what you would do differently if it were not.

Exercise 3 — Estimate the cost, define the SLOs and write the ADRs

Fill in the cost estimate table using the official calculator and compare it against your limit. If it does not fit, apply the measures in order and write the ADR for the cut. Define one or two SLOs with their SLI, target, window and error budget, plus the error budget policy. Write at least three ADRs using the complete template, including the negative consequences section and the one on what would change the decision.

Finish with the complete checklist from section 13 and answer the five control questions in writing.

Solutions

Solution 1 — RefugioReserva's architecture

Logical components filled in:

Logical component What it does in RefugioReserva
Entry point Serves refugioreserva.example over HTTPS with a valid certificate
Web application Searches availability, creates bookings, shows the warden's dashboard
Asynchronous jobs Nightly job that aggregates daily occupancy into BigQuery and anonymises expired contact details
Objects Refuge photos: original and thumbnail
Operational DB Refuges, bookings, reviews, users
Event transport Every booking created or cancelled emits an event
Analytical store Event history and occupancy aggregates
Visualisation Federation dashboard: occupancy, cancellations, sentiment
AI Sentiment score for the reviews
Identity and secrets 4 SAs, DB password and session key in Secret Manager
Observability Dashboard of the 4 signals, 2 alerts, availability SLO
Continuous delivery Cloud Build from GitHub with WIF
IaC Terraform with modules, state in GCS

The translation table and the three diagrams are the ones in section 14 and in section 4. One detail of the deployment diagram is worth flagging: after ADR-006, the load balancer disappears from the permanent state and Cloud Run is exposed with its custom domain, so the definitive deployment diagram has one box fewer and the Client → Cloud Run arrow goes directly. The diagram is updated when the decision changes; a diagram that contradicts the ADR is worse than having no diagram.

Solution 2 — RefugioReserva's network, identity and data

Addressing (prod):

Block CIDR Use
Prod space 10.20.0.0/16 Reserved in full
Application subnet 10.20.0.0/24 Reserved; no VMs today
Serverless connector 10.20.8.0/28 Cloud Run egress towards the VPC
Private services access 10.20.16.0/20 Peering for Cloud SQL
Free 10.20.32.0/19 Future

Dev uses 10.10.0.0/16 with the same structure. They do not overlap, on purpose: if one day I wanted to peer the two VPCs, I could; if they overlapped, I could not.

Flow matrix: the one in section 7, with two clarifications after ADR-006. Since there is no longer a load balancer, Cloud Run is configured with --ingress=all (required for the custom domain) and the protection moves to: authentication on the private routes, strict input validation, and --max-instances=5 as a cap on spend and on abuse. This loss is acknowledged in ADR-006 and will also appear in the 08-05 self-assessment as conscious technical debt.

Identities:

Identity Role Scope
sa-refugio-web roles/cloudsql.client project
roles/secretmanager.secretAccessor secrets refugio-db-password, refugio-session-key
roles/storage.objectAdmin bucket refugio-fotos-8f2a
roles/pubsub.publisher topic reservas-eventos
roles/logging.logWriter, roles/cloudtrace.agent project
sa-procesar-evento roles/bigquery.dataEditor dataset refugio_analitica
roles/pubsub.subscriber subscription reservas-eventos-sub
sa-job-nocturno roles/cloudsql.client project
roles/bigquery.jobUser project
roles/bigquery.dataEditor dataset refugio_analitica
sa-deploy roles/run.developer project
roles/artifactregistry.writer repository refugio-imagenes
roles/iam.serviceAccountUser only on sa-refugio-web and sa-job-nocturno

Four accounts, no primitive roles, no keys. sa-deploy is federated from GitHub with WIF restricted to the repository and to the main branch.

Data: the schemas from section 9. The lifecycle table includes one row worth highlighting:

Data Dies Mechanism Verification
Booker's contact details 6 months after the stay UPDATE reserva SET nombre_titular='anonimizado', email_titular='[email protected]' WHERE fecha < NOW() - INTERVAL '6 months' in the nightly job A query counting rows with contact details and an old date: it must return 0

That verification column is what turns the retention policy into something checkable. Without it, it is an intention.

Note on fictional data: the 2,000 records are generated with data/seed/generar.py by combining lists of invented names and @example.com emails (a domain reserved by RFC 2606 for precisely this). If the system processed real data, it would need: a legal basis for the processing, information for the data subject, a record of processing activities, a data processing agreement with Google Cloud, an impact assessment for processing the refuges' location data, and probably customer-managed encryption keys (CMEK) on Cloud SQL and on the buckets. None of that applies here because there is no real personal data, and that is exactly the reason there is none.

Solution 3 — RefugioReserva's cost, SLOs and ADRs

The estimate, the load balancer discovery and the complete ADR-006 are in section 14. The final table after the cut:

Service Cost/month
Cloud SQL prod (db-f1-micro) €8.00
Cloud SQL dev (ephemeral, ~20 h/month) €0.25
Cloud Run (prod + dev) €0.30
Cloud Storage €0.10
Artifact Registry €0.20
NL API (one time) €0.40
BigQuery, Pub/Sub, Functions, Logging €0.00
Domain (pro rata) €1.00
Subtotal €10.25
With 25 % headroom €12.81
Limit €12.00

Still just over. Additional measure applied, from the cuts table: overnight shutdown of production Cloud SQL between 01:00 and 08:00 via a scheduled job, which reduces that line item by 29 % (~€2.30). Final estimated cost: €10.51 with headroom, within the limit. The consequence — the application does not work between 1 and 8 in the morning — is documented in the README and declared explicitly in the SLO: the measurement window excludes that period, because an SLO breached by a planned and known shutdown measures nothing useful.

RefugioReserva's five ADRs:

ADR Decision Deciding factor
ADR-001 Cloud Run for the application Scales to zero with almost no traffic
ADR-002 Cloud SQL PostgreSQL over Firestore Capacity control with a transaction and a row lock
ADR-003 Pub/Sub + Cloud Function over Dataflow €70/month versus €0 for 60 messages a day
ADR-004 Natural Language API over AutoML Meets RF-6 in two hours; AutoML is days and euros
ADR-005 Three projects, europe-west1, refugio-* names Irreversible; decided before creating anything
ADR-006 No permanent load balancer The cost constraint is a requirement, not an obstacle

The five control questions, answered:

  1. How much will it cost? €10.51 a month. The biggest line item is Cloud SQL (€5.70 after the overnight shutdown), followed by the domain.
  2. What if the database goes down? Availability search and booking creation stop working — the entire critical journey. Still working: the static refuges page (cached), the bucket photos and the Looker Studio dashboard, which reads from BigQuery. The application must degrade with a clear message, not with a 500 error. This is a concrete reliability test for 08-04.
  3. Who can read the users' data? My personal account (project owner), sa-refugio-web (through cloudsql.client + the credentials from the secret) and sa-job-nocturno. Nobody else. sa-procesar-evento and sa-deploy do not have access to Cloud SQL, and the analytical store contains no contact details.
  4. What did I reject in the most important decision? In ADR-002, Firestore. It would have saved €8/month and all the private networking work, but capacity control under concurrency — the heart of the problem — would have been noticeably more fragile without row-locking transactions. I accepted the cost and offset it with the overnight shutdown.
  5. How do I find out something is wrong? An email alert when the 5xx error rate exceeds 5 % for 5 minutes, an alert on SLO error budget consumption at 50 %, an uptime check every 5 minutes against /salud, and a billing budget alert at 50/90/100 % of €12.

Conclusion

You have the blueprint. And, more importantly, you have the decisions taken and written down before it cost money to undo them.

You know why you design before typing: not out of academic rigour, but because there are decisions — projectId, region, CIDR, database engine, partition key — that cost ten seconds now and a complete rebuild later. And you know what "enough design" is: nine artefacts, five to eight hours, none of them more than a page, and the proof that it is finished is the five control questions.

You know how to go from requirements to logical components before naming a single Google product, and from there to services using the course's decision trees — 02-07 for compute, 02-06 for data, 04-01 versus 04-04 for analytics — always documenting the four columns: what you choose, why, what you reject and why. That fourth column is half of block A of the rubric and the literal question in an interview.

You know how to draw the three diagrams that are worth having — context, components, deployment — each with one question and one level of detail, with the 15-box rule and the five readability rules, and in mermaid inside the repository so they age with the code and not in a forgotten PNG.

You have decided the organizational structure: how many projects and why two is the right balance, the region with its reason written down, the naming convention with GCP's hard rules (the unrepeatable projectId, the global bucket name) and the labels, with gestionado-por=terraform as a free drift detector.

You have the network designed on paper: the addressing plan with the connector's /28 and the private services access range everybody forgets, the flow matrix with deny by default, and the public surface in five lines.

You have the identity in a table written before creating anything: one service account per workload, no primitive roles, every permission at the narrowest scope, WIF without keys and serviceAccountUser accounted for so that the first automated deployment does not give you an inexplicable 403.

You have the data model with what lives where and why, the operational DDL with its constraints and its partial index, the partitioned analytical table with require_partition_filter as a seatbelt for the bill, and — what almost nobody designs — the complete lifecycle, including deletion, with its mechanism and its verification query, and with the GDPR warning that turns discipline into habit even when your data is invented.

You have the cost estimate done before spending a euro, with the four money sinks identified and the cuts table in order. And you have the RefugioReserva example, where the estimate revealed that the load balancer cost three times the entire budget: exactly the discovery that estimating beforehand is for.

You have your SLOs defined with their SLI, their realistic target, their window and their calculated error budget, plus the policy that turns that budget into decisions.

And you have your ADRs, with the complete template and with the two sections most people leave weak: the negative consequences — because every decision loses something, and acknowledging it is judgement — and what would change the decision, which is what keeps it alive.

In the next lesson, 08-03, you start building. And you will do it in a specific, justified order — organization, network, identity, Terraform state, data, application, delivery, analytics, AI, observability — with a phase 0 of preparation, eight phases each with its verifiable "done" criterion, the cross-cutting practices that stop the project deforming along the way, the five-step method for when you get stuck, and the most honest warning in the module: the first time, everything takes twice as long.

Save the design in the repository and commit. From here on, everything you create must correspond to something that is already in this document.

Google Cloud Platform (GCP) Course

Module 1: Introduction to Google Cloud Platform

Module 2: Core GCP Services

Module 3: Networking and Security

Module 4: Data and Analytics

Module 5: Machine Learning and AI

Module 6: DevOps and Monitoring

Module 7: Advanced GCP Topics

Module 8: Final Project

© Copyright 2026. All rights reserved