Technology Selection Matrices

Quote

“Choose boring technology. Every technology choice carries an innovation token cost — spend them wisely.”

Dan McKinley, “Choose Boring Technology” (2015)

Language Selection: Which Language for Which Task

Primary Decision Matrix

TaskBest ChoiceWhyAvoid
Data transformation logicSQL (in-database)Pushes compute to the engine, no data movement, optimizer parallelizesPython for simple transforms that SQL handles natively
Complex business logic with typesPythonRich libraries, readable, testable, type hints availableBash (untyped, fragile for multi-step logic)
File operations on Linux serversBashNative, fast, zero runtime overhead, pipes compose naturallyPython (overhead for simple file ops like mv/cp/chmod)
Windows automationPowerShellNative .NET integration, Windows Task Scheduler, registry accessBash (WSL adds complexity and failure modes)
High-performance API servingC# (ASP.NET) / Python (FastAPI)Compiled speed for C#, async Python with Pydantic validationBash (not an API language, no HTTP framework)
Ad-hoc data explorationPython (pandas/Polars)Interactive REPL, visual output, rapid prototypingC# (too verbose for throwaway exploration)
SQL Server administrationT-SQL + PowerShellNative tooling, dbatools module, SSMS integrationPython (no advantage over native tools for DBA tasks)
GCP infrastructure opsBash + gcloud CLIDirect CLI, scriptable, well-documented flagsPowerShell (gcloud SDK is bash-native, PS wrapper adds friction)
Pipeline orchestration gluePythonAirflow is Python, rich GCP client libraries, testableBash (hard to unit test, poor error handling patterns)
One-liner text processingBash (awk/sed/grep)Fastest path from data to answer, no boilerplatePython (overhead for grep-level tasks)
ML feature engineeringPython (scikit-learn/numpy)Ecosystem has no peer for ML preprocessingSQL (limited to what the engine supports)
Configuration managementYAML/JSON + PythonStructured, version-controllable, parseableHardcoded values in any language
Docker image buildsBash (in Dockerfile)Native to the build context, minimal layersPython scripts inside Dockerfiles (unnecessary complexity)
CI/CD pipeline scriptsBash + PythonGitHub Actions runs bash natively, Python for complex stepsPowerShell in Linux CI runners (compatibility issues)
Database migrationsSQL (via dbt or migration tool)Schema changes belong in SQL, version-controlledPython ORM migrations for data warehouse schemas
Log parsing (structured)Python (json module)Handles nested JSON, filtering, aggregationBash (jq works for simple cases but breaks on complex nesting)
Log parsing (unstructured)Bash (grep/awk) then PythonGrep for finding, Python for extracting patternsStarting with Python when grep would answer the question

Detailed Comparison: Python vs Bash

Two languages every data engineer uses daily. The question is never “which one” — it is “which one for this task.”

DimensionBashPython
Startup time~5ms~50-100ms (interpreter + imports)
File manipulationNative (mv, cp, chmod, find)os/shutil/pathlib (works but verbose)
Process managementNative (ps, kill, nohup, &)subprocess module (works but wraps bash)
CLI tool chainingPipes are first-class (`cmd1cmd2
Error handlingset -euo pipefail (fragile beyond that)try/except with full stack traces
Data structuresArrays only (no dicts, no objects, no nesting)Lists, dicts, sets, classes, dataclasses
String manipulationParameter expansion (${var##*/})Full string methods, regex, f-strings
Testingbats-core (limited)pytest (world-class)
API callscurl (works for simple GETs)requests/httpx (handles auth, pagination, retries)
PortabilityLinux/macOS (bash 4+ not default on macOS)Cross-platform
Debuggingset -x, echo statementspdb, IDE debuggers, logging module

When Bash wins

  • File manipulation: mv, cp, chmod, find, rsync
  • Process management: ps aux, kill, nohup, background jobs
  • CLI tool chaining: gcloud ... | jq ... | xargs ...
  • Cron jobs under 20 lines: extract a file, compress, upload to GCS
  • Quick-and-dirty log parsing: grep ERROR /var/log/app.log | tail -20
  • Environment setup: .bashrc, .profile, export chains

When Python wins

  • Anything with conditionals beyond simple if/else
  • Loops that process data (not just files)
  • Error handling that needs to be reliable
  • API calls with authentication, pagination, retry logic
  • Data frames, CSVs, JSON manipulation
  • Anything that needs unit tests
  • Any script another engineer will maintain

The 50-Line Rule

If your bash script exceeds 50 lines, rewrite it in Python. Bash scripts over 50 lines become unmaintainable — they accumulate quoting bugs, lack proper error handling, and become impossible to test. The rewrite takes an hour; the debugging you avoid saves days.

Rewrite Long Bash Scripts in Python with Proper Structure

When a bash script reaches 50 lines, migrate it to Python: replace set -euo pipefail error handling with try/except blocks, replace untyped variables with typed function parameters, replace echo debugging with the logging module, and add a pytest test file alongside it. The resulting Python script is testable, type-checked by mypy, and maintainable by any engineer on the team — not just the one who wrote the original bash.

Example: Parsing a CSV

The same task, two approaches — and when each is right:

# Bash: extract column 3 where column 1 matches "ACTIVE"
# RIGHT when: one-off investigation, piping to another command
awk -F',' '$1 == "ACTIVE" {print $3}' data.csv | sort | uniq -c | sort -rn
# Python: same logic but with error handling and reuse
# RIGHT when: production pipeline, needs testing, output goes to a database
import pandas as pd
df = pd.read_csv("data.csv")
result = df[df["status"] == "ACTIVE"]["value"].value_counts()

The bash version is 1 line. The Python version is 3 lines plus imports. But when the CSV has quoted fields containing commas, the bash version silently produces wrong results while the Python version handles it correctly.

Detailed Comparison: Python vs C#

DimensionPythonC#
TypingDynamic (optional type hints)Static (compiled)
Startup~100ms~200ms (JIT), <50ms (AOT/trimmed)
ThroughputModerate (GIL limits CPU parallelism)High (true multi-threading, async/await)
Data librariespandas, Polars, numpy, scipy, scikit-learnLINQ, Entity Framework, Dapper
API frameworksFastAPI, FlaskASP.NET Core (Kestrel)
GCP SDKsgoogle-cloud-* (first-class)Google.Cloud.* (good but less documented)
Airflow integrationNative (DAGs are Python)None (must wrap via BashOperator or API calls)
Dashboard/UIStreamlit, Dash (adequate)Blazor (enterprise-grade)
Package managementpip/poetry/uv (improving)NuGet (mature, reliable)
IDE experienceVS Code + Pylance (good)Visual Studio / Rider (excellent)
Learning curveLow (readable, forgiving)Medium (more ceremony, but clearer contracts)

When C# wins

  • High-throughput APIs serving thousands of requests/second
  • Windows services that run as background daemons
  • Blazor dashboards with server-side rendering
  • Strong typing at scale (100+ files, multiple contributors)
  • Enterprise middleware integrating with .NET ecosystem
  • Performance-critical code paths (no GIL, true parallelism)

When Python wins

  • Data manipulation and transformation pipelines
  • ML model training and inference
  • Rapid prototyping (half the lines of C# for equivalent logic)
  • Airflow DAG authoring (Python is the only option)
  • GCP client library usage (better docs, more examples)
  • Notebook-driven analysis and exploration

The Hybrid Pattern

Use Python for pipeline logic (Airflow DAGs, data transforms, GCP orchestration) and C# for the serving layer (Blazor dashboards, high-throughput APIs via Dapper + ASP.NET). This is not compromise — it is using each language where it excels. See database connections for connection patterns in both languages.

Detailed Comparison: SQL vs Python for Data Transforms

This is the most frequent decision a data engineer makes. The answer is almost always SQL — until it is not.

DimensionSQL (in-database)Python (application layer)
AggregationsNative, optimized, parallelizedpandas groupby (works but slower)
JoinsNative, uses index seeks and hash joinspandas merge (loads both sides into memory)
Window functionsROW_NUMBER, LAG, LEAD, running totalspandas .rank(), .shift() (equivalent but slower)
GROUP BYHash/sort aggregation on the enginegroupby().agg() (works, but why move the data?)
String parsingSUBSTRING, CHARINDEX, PATINDEX (limited)regex, string methods (far more powerful)
API enrichmentNot possiblerequests/httpx during transform
ML featuresLimited (basic math only)scikit-learn, numpy (full ecosystem)
Cross-databaseNot possible (within one engine only)pandas reads from multiple sources
TestabilitytSQLt, dbt testspytest (more flexible)
Version controldbt models, migration filesStandard Python modules
Performance at 1M rowsSub-second (indexed)Seconds (pandas), sub-second (Polars)
Performance at 1B rowsSeconds to minutes (partitioned)Out of memory (pandas), minutes (Polars/Spark)

The SQL-First Rule

“If you can express it in SQL, do it in SQL. The database is faster than your code.”

The database engine has a query optimizer that has been refined over decades. It knows the data distribution, has indexes, can parallelize across cores, and operates directly on compressed columnar storage. Your Python code pulls data over a network, deserializes it, processes it in a single thread (GIL), and sends it back. The only exception is when the transform requires something SQL cannot do (API calls, ML, cross-database joins, complex regex).

dbt as the bridge: dbt-transformation-layer lets you write SQL transforms but manage them with software engineering practices — version control, testing, documentation, dependency graphs. This gives you SQL’s performance with Python-level engineering discipline.

When SQL wins (always prefer for these)

  • Aggregations: SUM, AVG, COUNT, MIN, MAX
  • Joins: INNER JOIN, LEFT JOIN, CROSS APPLY
  • Window functions: ROW_NUMBER(), LAG(), LEAD(), SUM() OVER
  • Filtering: WHERE, HAVING
  • Deduplication: ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) = 1
  • Pivoting: PIVOT / UNPIVOT
  • Date arithmetic: DATEADD, DATEDIFF, DATE_TRUNC
  • Type conversion: CAST, CONVERT, TRY_CAST

When Python wins (SQL cannot do these well)

  • Calling external APIs during a transform
  • Complex regex beyond PATINDEX capability
  • ML feature engineering (z-scores, normalization, encoding)
  • Cross-database joins (SQL Server table + BigQuery table)
  • Reading non-database sources (APIs, files, streams)
  • Custom business logic with 10+ conditional branches
  • Fuzzy string matching (Levenshtein, phonetic)

PowerShell: When and Why

PowerShell occupies a specific niche. It is not a general-purpose scripting language — it is the Windows automation language.

Use CasePowerShell?Alternative
Windows Task Scheduler automationYes
SQL Server administration (dbatools)YesT-SQL for query-level ops
Active Directory / Windows ServerYes
.NET object manipulationYesC# for complex cases
File operations on WindowsYesBash (via WSL) for Linux-style ops
GCP operationsNoBash + gcloud
Data pipeline logicNoPython
CI/CD on Linux runnersNoBash
Cross-platform scriptingNoPython or Bash

PowerShell on Linux

PowerShell Core (pwsh) runs on Linux, but the ecosystem assumes Windows. gcloud, Docker, kubectl, and terraform are all bash-first tools. Using PowerShell on Linux adds friction without benefit. Reserve PowerShell for Windows-specific automation. See windows-scheduling for Task Scheduler patterns.


Database Selection: Which Database for Which Workload

Primary Decision Matrix

FactorSQL Server (on VM)BigQueryCloud SQL (PostgreSQL)FirestoreBigtable
Best forOLTP + OLAP hybrid, existing SQL Server shopsAnalytics at any scale, ML, BI backendSmall-medium OLTP, PostgreSQL ecosystemReal-time state, config, feature flagsTime-series at massive scale, IoT
Scaling modelVertical (bigger VM)Automatic (serverless)Vertical + read replicasAutomatic (serverless)Horizontal (add nodes)
Cost modelVM + license (or Linux free)Per TB scanned + storagePer instance hour + storagePer operation + storagePer node hour + storage
SchemaFixed (relational, normalized or star)Fixed (columnar, nested STRUCT/ARRAY)Fixed (relational)Flexible (document, nested)Column families (wide-column)
Query latency<1ms (indexed OLTP), seconds (OLAP)1-30s (cold), <5s (BI Engine cached)<5ms (indexed)<10ms (single doc)<10ms (single row)
Max practical data~16 TB per databasePetabytes~64 TB~1 TB practical (1 MB per doc)Petabytes
ManagedNo (self-managed on Compute Engine)Fully managedFully managedFully managedFully managed
ACID transactionsFull (serializable isolation)DML transactions (limited)Full (serializable isolation)Document-level, multi-doc with limitsSingle-row only
Stored proceduresYes (T-SQL, extensive)Yes (SQL, JavaScript UDFs)Yes (PL/pgSQL)No (use Cloud Functions)No
Backup/DRManual (you manage)Automatic (time travel)Automatic (managed backups)Automatic (multi-region)Automatic (replication)
EcosystemSSMS, dbatools, SSIS, SSRSbq CLI, Python SDK, Looker, dbtpgAdmin, psql, rich extension ecosystemFirebase SDK, Firestore SDKcbt CLI, HBase API

Database Decision Flowchart

Follow this top-down. The first “yes” is your answer.

1. Do you need sub-10ms reads for real-time serving?
   ├── Yes → Is the data key-value or document-shaped?
   │         ├── Yes → Firestore (see [firestore-data-model-and-operations](https://alp78.github.io/elysium/06-GCP/Firestore/firestore-data-model-and-operations))
   │         └── No → Is the data time-series at >1 TB?
   │                   ├── Yes → Bigtable
   │                   └── No → SQL Server or Cloud SQL (indexed reads)
   └── No ↓

2. Do you need complex SQL analytics (joins, window functions, GROUP BY)?
   ├── Yes → Is the data >10 TB or growing unpredictably?
   │         ├── Yes → BigQuery (see [querying-and-cost-optimization](https://alp78.github.io/elysium/06-GCP/BigQuery/querying-and-cost-optimization))
   │         └── No → SQL Server (if you have it) or BigQuery (if starting fresh)
   └── No ↓

3. Do you need ACID transactions with stored procedures?
   ├── Yes → Do you have existing SQL Server expertise?
   │         ├── Yes → SQL Server (see [moc-sql-server](https://alp78.github.io/elysium/04-Databases/moc-sql-server))
   │         └── No → Cloud SQL PostgreSQL
   └── No ↓

4. Do you need a document store for application state?
   ├── Yes → Firestore
   └── No ↓

5. Default: BigQuery for analytics, Firestore for state, SQL Server if already in place.

Migration Cost Is Real

If you already run SQL Server and it handles your workload, the cost of migrating to BigQuery or Cloud SQL is measured in months of engineering time, regression testing, and retraining. “Better” technology does not justify migration unless the current system is failing. The decision to migrate should be driven by a specific pain point (cost, scale, features), not by preference.

Justify Migration with a Specific Pain Point, Not Preference

Before proposing any database migration, document the specific pain point driving it: a query that takes 4 hours in SQL Server and 5 minutes in BigQuery, a cost that exceeds a defined threshold, a scaling ceiling that is being hit in production. If no such pain point exists, document the decision to stay and revisit at the next annual architecture review. Migrations driven by preference rather than pain consistently deliver disappointment.

Detailed Comparison: SQL Server vs BigQuery

DimensionSQL Server (on GCE VM)BigQuery
Query modelRow-oriented, B-tree indexesColumnar, full-scan oriented
Optimization strategyCreate indexes, avoid scansPartition, cluster, avoid SELECT *
ConcurrencyHundreds of connectionsThousands of concurrent queries
Cost at 1 TB~$200/mo (VM) + license~$5/mo storage + per-query cost
Cost at 100 TB~$2,000/mo (large VM) + license~$500/mo storage + per-query cost
Real-time insertsYes (INSERT, MERGE)Yes (streaming inserts, $0.05/GB)
Batch loadsBULK INSERT, BCP, SSISbq load, GCS → BigQuery (free)
Stored proceduresExtensive (T-SQL)Limited (SQL scripting, JS UDFs)
Security modelLogins, roles, schemas, RLS, TDEIAM, column-level, row-level, VPC-SC
MonitoringDMVs, wait stats, Datadog integrationINFORMATION_SCHEMA, Cloud Monitoring
BackupFull/diff/log backups (you manage)Automatic time travel (7 days free)
Best atTransactional + analytical hybridPure analytical at any scale

See backup-types-and-strategy for SQL Server backup patterns. See querying-and-cost-optimization for BigQuery cost control.

When to Add a Second Database

You need a second database when one database cannot serve two workloads without conflict:

SignalAction
Analytical queries blocking OLTP insertsOffload analytics to BigQuery
Real-time UI reads competing with batch loadsAdd Firestore for UI state
Time-series data growing beyond SQL Server capacityAdd Bigtable for metrics
Multiple teams need different query patternsBigQuery for analytics, SQL Server for operations
Need to join external data (APIs, files) with warehouse dataBigQuery (external tables, federated queries)

The Two-Database Rule

Most data engineering teams need exactly two databases: one for OLTP (SQL Server or Cloud SQL) and one for OLAP (BigQuery). Anything beyond two requires strong justification. Each additional database adds operational overhead: backups, monitoring, security, connection management, and a new failure mode.


GCP Component Selection

Compute: When to Use What

NeedUse ThisWhy Not Alternatives
Long-running stateful service (SQL Server, Airflow)Compute Engine VMCloud Run has 1h timeout; App Engine is for stateless web apps
Batch pipeline job (extract, transform, load)Cloud Run JobNo idle cost; scales to zero; Docker-native. See cloud-run-jobs-vs-services
HTTP API endpointCloud Run ServiceAuto-scales, managed TLS, custom domains, 0 to N instances
Schedule a triggered jobCloud Scheduler + Cloud RunDo not run a VM 24/7 for a job that runs 3 times per day. See gcp-scheduling
Heavy Spark/Hadoop processingDataprocWhen you need distributed compute beyond a single container
Stream processingDataflow (Apache Beam)Managed, auto-scaling, exactly-once semantics. See streaming-architecture
Lightweight event-driven functionCloud FunctionsCold start latency is acceptable, function completes in <9 min
GPU workloads (ML training)Compute Engine + GPU or Vertex AICloud Run does not support GPUs
Long-running batch (>1h)Compute Engine VM (ephemeral)Cloud Run Job max 1h; use preemptible VMs for cost savings

Compute Decision Flowchart

1. Does it need to run 24/7?
   ├── Yes → Is it stateful (data on disk)?
   │         ├── Yes → Compute Engine VM (see [vm-lifecycle](https://alp78.github.io/elysium/06-GCP/Compute/vm-lifecycle))
   │         └── No → Cloud Run Service (min-instances=1 if needed)
   └── No ↓

2. Is it triggered by an HTTP request?
   ├── Yes → Cloud Run Service
   └── No ↓

3. Is it triggered by a schedule?
   ├── Yes → Cloud Scheduler → Cloud Run Job (see [gcp-scheduling](https://alp78.github.io/elysium/12-Orchestration/Scheduling/gcp-scheduling))
   └── No ↓

4. Is it triggered by a Pub/Sub message?
   ├── Yes → Cloud Run Service (Pub/Sub push) or Cloud Functions
   └── No ↓

5. Does it need >1 hour of execution time?
   ├── Yes → Compute Engine VM (ephemeral, preemptible)
   └── No → Cloud Run Job

The Zero-Idle-Cost Principle

If a workload runs less than 50% of the time, it should not be on a VM. Cloud Run Jobs and Cloud Functions scale to zero. A VM running 24/7 for a job that runs 3 times per day wastes 99.9% of its uptime cost. See finops-cost-optimization for cost analysis patterns.

Messaging: Pub/Sub vs Direct Calls vs Firestore

ScenarioUse ThisWhy
Decouple producer and consumerPub/SubProducer does not need to know who consumes
Fan-out to multiple consumersPub/Sub (multiple subscriptions)One message, N subscribers. See pubsub-messaging
Buffer traffic burstsPub/SubMessages queue; consumers process at their pace
At-least-once delivery guaranteePub/SubBuilt-in acknowledgment and retry
Synchronous request-responseDirect HTTP/gRPCCaller needs the response immediately
Single known consumerDirect HTTPPub/Sub adds unnecessary indirection
Low latency required (<100ms)Direct HTTP/gRPCPub/Sub adds 50-200ms per hop
Real-time UI state updatesFirestoreReal-time listeners push to clients. See firestore-data-model-and-operations
Config propagation across servicesFirestoreAll services watch the same document
Dead letter handlingPub/Sub (dead letter topic)Failed messages route to a DLQ for investigation
Ordered message processingPub/Sub (ordering key)Messages with the same key processed in order

The Pub/Sub Default

When in doubt between Pub/Sub and direct calls, choose Pub/Sub. The decoupling it provides is almost always worth the added complexity. The exception is when you need synchronous responses or sub-100ms latency. See pubsub-topics-and-subscriptions for configuration patterns.

Storage: GCS vs BigQuery vs SQL Server

Every piece of data lives somewhere. The question is where, and the answer depends on how the data will be consumed.

Data TypeStore InWhyFormat
Raw API responses (landing zone)GCSCheap, immutable, reprocessableJSON / NDJSON
Raw file extracts (CSV, Excel)GCSLanding zone before transformationOriginal format
Transformed analytical dataBigQueryQuery engine optimized for analyticsNative tables
Transactional operational dataSQL ServerACID, low-latency reads, stored procsRelational tables
Pipeline intermediate artifactsGCSTemporary, disposable, cheapParquet (see serialization-formats)
Large objects (images, PDFs, binaries)GCSObject storage, no size limitsOriginal format
Archived historical dataGCS (Coldline/Archive)$0.004/GB/mo, 90-day minimumParquet (compressed)
ML training datasetsGCS → BigQueryGCS for storage, BigQuery for feature queriesParquet or TFRecord
Dashboard-ready aggregationsBigQuery (materialized views)Pre-computed, auto-refreshedNative tables
Application state and configFirestoreReal-time reads, document-orientedDocuments

The Medallion Mapping

How storage maps to the medallion-architecture layers:

LayerPrimary StorageSecondaryPurpose
Bronze (raw)GCS (landing zone)SQL Server (raw tables)Exact copy of source data, immutable
Silver (cleaned)SQL Server or BigQueryDeduplicated, typed, validated
Gold (business)BigQuery or SQL ServerFirestore (for serving)Business aggregations, KPIs, features

The Single Source of Truth Rule

Each dataset has exactly one authoritative storage location. Other locations are copies, caches, or materializations. When copies drift from the source, the source wins. Document the authoritative location for every dataset in your catalog. See context-and-metadata-architecture for metadata patterns.

Networking and Security Selection

NeedUse ThisNotes
Secure access to VMs from laptopIAP tunnelingNo public IP needed. See iap-tunneling
Service-to-service authenticationService accountsWorkload identity for GKE. See service-accounts-and-iam
Restrict data access to VPCVPC Service ControlsPrevents data exfiltration. See vpc-service-controls
Encrypt data at rest (SQL Server)TDETransparent Data Encryption. See tde-encryption
API authenticationOAuth 2.0 / API keysOAuth for user-context, API keys for service-context
Secret managementSecret ManagerNever hardcode credentials. See iam-and-secrets
Network between VMsVPC + firewall rulesLeast-privilege firewall rules. See firewalls

Orchestration Selection: When to Use What

Primary Decision Matrix

FactorAirflow (self-hosted)Cloud ComposerCron (Linux)Cloud SchedulerWindows Task Scheduler
Best forComplex DAGs with dependencies, retries, branchingSame as Airflow, fully managedSimple recurring tasks, single-serverTriggering Cloud Run/Functions on scheduleWindows-only scheduled tasks
Monthly cost~$50-150 (VM)$300+ minimumFree~$0.10/job/monthFree (Windows license)
Dependency managementYes (DAG graph, sensors, triggers)Yes (same engine)NoNoNo
Retry logicBuilt-in (configurable per task)Built-inManual (wrapper script)HTTP retry onlyBasic retry settings
AlertingEmail, Slack, PagerDuty (via callbacks)Same + Cloud MonitoringManual (mail/curl in script)Cloud MonitoringWindows Event Log
UIWebserver dashboard with Gantt chartsSame (managed)NoneConsole onlyTask Scheduler GUI
Backfill supportYes (date-parameterized runs)YesManualManualManual
LoggingTask logs in webserver UISame + Cloud Loggingstdout/stderr to filesCloud LoggingWindows Event Log
Dynamic DAGsYes (generate DAGs from config)YesNoNoNo
Cross-task data passingXComs (small data), GCS (large data)SameFiles, environment variablesHTTP payloadFiles, registry
Setup complexityMedium (Docker Compose or VM install)Low (managed)TrivialLowLow
Maintenance burdenMedium (upgrades, DB cleanup, log rotation)Low (Google manages)MinimalNoneMinimal

Orchestration Decision Flowchart

1. How many scheduled jobs do you have?
   ├── <5, no dependencies between them
   │   ├── All on Linux → cron (see [linux-scheduling](https://alp78.github.io/elysium/12-Orchestration/Scheduling/linux-scheduling))
   │   ├── All on Windows → Task Scheduler (see [windows-scheduling](https://alp78.github.io/elysium/12-Orchestration/Scheduling/windows-scheduling))
   │   └── Triggering GCP services → Cloud Scheduler (see [gcp-scheduling](https://alp78.github.io/elysium/12-Orchestration/Scheduling/gcp-scheduling))
   └── 5+ jobs, OR dependencies exist ↓

2. Do jobs have dependencies (Job B waits for Job A)?
   ├── No → Cloud Scheduler + Cloud Run Jobs (one per job)
   └── Yes ↓

3. What is your monthly budget for orchestration?
   ├── <$300/mo → Self-hosted Airflow on a VM (see [airflow-deployment](https://alp78.github.io/elysium/12-Orchestration/Airflow/airflow-deployment))
   └── $300+/mo → Cloud Composer (if team is 3+ people)

4. Do you need backfill capability (re-run for past dates)?
   ├── Yes → Airflow (self-hosted or Composer)
   └── No → Cloud Scheduler may suffice even with light dependencies

The Orchestration Escalation Path

Start simple, escalate when forced:

  1. Cron/Cloud Scheduler — single jobs, no dependencies
  2. Self-hosted Airflow — dependencies appear, need backfills, 5-20 DAGs
  3. Cloud Composer — 20+ DAGs, team of 3+, want managed infrastructure

Do not start with Cloud Composer. Its 50-100/month. See airflow-core-concepts for DAG design patterns.

Airflow-Specific Decisions

DecisionRecommendationWhy
Executor type (self-hosted)LocalExecutor for <20 DAGs, CeleryExecutor for 20+LocalExecutor is simpler; Celery adds worker scaling
Database backendPostgreSQLSQLite is single-writer only; MySQL works but PostgreSQL is better supported
Run Airflow in Docker?Yes (Docker Compose)Reproducible, version-pinned, easy upgrades. See airflow-deployment
Store DAGs in Git?Yes (always)DAGs are code; they belong in version control
Trigger DAGs externally?Airflow REST APIBetter than SSH + airflow dags trigger
Pass data between tasks?XComs for <48 KB, GCS for largerXComs stored in Airflow DB; large data chokes the DB. See airflow-dag-patterns
Monitor DAG health?Datadog Airflow integrationMetrics on task duration, failure rate, queue depth. See datadog-airflow-observability

Event-Driven Orchestration (Not Scheduled)

When work is triggered by events rather than time:

TriggerMechanismUse Case
File lands in GCSGCS notification → Pub/Sub → Cloud RunProcess uploaded files on arrival
Database row changesCDC → Pub/Sub → consumerReal-time replication. See streaming-architecture
API webhook receivedCloud Run Service (HTTP endpoint)SaaS integration (Stripe, GitHub, etc.)
Airflow DAG completesTriggerDagRunOperator or Pub/SubChain DAGs across Airflow instances
Manual trigger (ad hoc)Airflow UI or REST APIBackfills, one-off reprocessing

Infrastructure: Terraform vs gcloud CLI vs Console

Primary Decision Matrix

ScenarioUse ThisWhy
Production infrastructureTerraformReproducible, version-controlled, reviewable in PRs
Quick one-off investigationgcloud CLIFast, no state file management needed
Learning / exploring a new serviceConsole (UI)Visual, discoverable, no syntax to learn
CI/CD pipeline operationsgcloud CLIScriptable, idempotent commands, GitHub Actions native
Disaster recovery rebuildTerraformterraform apply rebuilds the entire environment
Temporary dev/test resourcesgcloud CLICreate, test, delete — no state to clean up
Multi-environment (dev/staging/prod)Terraform (workspaces or directories)Same code, different variables per environment
IAM and security configurationTerraformAuditable history of who has access to what
One-time data migrationgcloud CLI or bq CLITerraform is overkill for a one-shot operation
Networking (VPC, firewall, subnets)TerraformNetwork changes are high-risk; review process required

The Infrastructure Rule

The Terraform Rule

  • If it exists in production, it is in Terraform. No exceptions. If someone creates a resource via Console or gcloud and does not add it to Terraform, it will drift, be forgotten, and eventually cause an incident.
  • If it is temporary, use gcloud CLI. Do not pollute Terraform state with throwaway resources.
  • If you are learning, use Console. Then translate to Terraform once you understand the resource.

See plan-apply-destroy for the apply workflow and moc-terraform for the full IaC reference.

Terraform-Specific Decisions

DecisionRecommendationWhy
State backendGCS bucketRemote, lockable, versioned. See state-management
Module structureOne module per logical resource groupVM + disk + firewall = one module. See module-composition
Variable managementtfvars files per environmentdev.tfvars, prod.tfvars — same code, different values
Secret handlingSecret Manager (referenced, not stored in state)Never put secrets in tfvars or state. See iam-and-secrets
Plan reviewAlways terraform plan before applyNo blind applies. Review the diff.
Import existing resourcesterraform import + write matching configBrings Console-created resources under management
Provider versioningPin major + minor version~> 5.0 allows patch updates, blocks breaking changes
CI/CD integrationGitHub Actions: plan on PR, apply on mergeSee github-actions-ci-cd for workflow patterns

gcloud CLI: When It Shines

Taskgcloud Command PatternWhy Not Terraform
Check VM statusgcloud compute instances describeRead-only; no state change
Tail logsgcloud logging readEphemeral query, not infrastructure
Deploy Cloud Run (dev)gcloud run deployFaster iteration than terraform apply
List IAM bindingsgcloud projects get-iam-policyAudit, not management
SSH into VMgcloud compute sshSession, not infrastructure
Upload to GCSgsutil cp / gcloud storage cpData operation, not infra
BigQuery querybq queryData operation, not infra
One-time service enablegcloud services enableIf done once and never changed

See gcloud-cli-setup for SDK command structure and gcp-apis-and-services for service enablement commands.


Data Model Selection

Quick decision table for choosing a data modeling approach. Each links to the detailed note with full DDL examples.

Your SituationModelKey CharacteristicsDetailed Reference
Building a BI warehouse with known queriesStar schema (Kimball)Fact + dimension tables, optimized for JOIN + GROUP BYdimensional-modeling
Enterprise with many source systems, need auditabilityData Vault 2.0Hub-link-satellite, handles schema changes gracefullydata-modeling-patterns
Fast dashboards from a single wide tableOne Big Table (OBT)Fully denormalized, no joins at query timedata-modeling-patterns
Event analytics, audit trails, clickstreamActivity schemaEntity + activity + timestamp, append-onlydata-modeling-patterns
Market data, IoT sensor data, system metricsTime-seriesTimestamp-partitioned, append-heavy, range queriesdata-modeling-patterns
Application state, feature flags, user configDocument (Firestore)Nested JSON-like structure, flexible schemafirestore-data-model-and-operations
Relationship analysis (fraud detection, social graphs)GraphNodes + edges, optimized for traversal queriesdata-modeling-patterns
Multi-layer analytics pipelineMedallion (Bronze/Silver/Gold)Layered refinement from raw to business-readymedallion-architecture

Model Selection Flowchart

1. Is the primary consumer a BI tool (dashboards, reports)?
   ├── Yes → Star schema (Kimball). Period.
   └── No ↓

2. Is the data time-series (timestamped measurements)?
   ├── Yes → Time-series model with timestamp partitioning
   └── No ↓

3. Is the data event-based (user actions, system events)?
   ├── Yes → Activity schema
   └── No ↓

4. Are there many source systems feeding one warehouse?
   ├── Yes → Data Vault 2.0 for the integration layer, star schema for the presentation layer
   └── No ↓

5. Is this application state (not analytics)?
   ├── Yes → Document model (Firestore)
   └── No ↓

6. Default: Star schema for analytics, document for application state.

The Modeling Principle

Model for the consumer, not the source. A BI analyst needs star schemas. An application needs document access patterns. An ML engineer needs wide feature tables. Start from how the data will be read and work backward to how it should be stored.


API Protocol Selection

Quick decision table for choosing an API protocol. Each links to the detailed note with implementation patterns.

Your SituationProtocolLatencyThroughputDetailed Reference
Consuming external vendor APIsREST~100-500msModeraterest-api-design-and-consumption
High-throughput internal service communicationgRPC~1-10msVery high (streaming, binary)grpc-for-data-pipelines
Flexible data queries from multiple consumersGraphQL~50-200msModerategraphql-for-data-access
Real-time bidirectional data feedWebSocket~1-5msHigh (persistent connection)api-protocols-comparison
Receiving push events from SaaS platformsWebhookN/A (push)Depends on senderapi-protocols-comparison
Batch data transfer between systemsFile-based (GCS)MinutesVery high (bulk)serialization-formats

Protocol Decision Flowchart

1. Are you consuming a third-party API?
   ├── Yes → Use whatever they offer (usually REST)
   └── No (building your own) ↓

2. Is this internal service-to-service?
   ├── Yes → Is latency critical (<10ms)?
   │         ├── Yes → gRPC (binary, streaming)
   │         └── No → REST (simpler, more tooling)
   └── No (external-facing) ↓

3. Do multiple consumers need different data shapes?
   ├── Yes → GraphQL (flexible queries)
   └── No ↓

4. Do you need real-time bidirectional communication?
   ├── Yes → WebSocket
   └── No → REST (default for external APIs)

REST Is the Default

When in doubt, use REST. It has the widest tooling support, the most documentation, and every engineer knows how to consume it. Only deviate to gRPC (for internal performance), GraphQL (for flexible querying), or WebSocket (for real-time bidirectional) when REST’s limitations are specifically blocking you. See api-protocols-comparison for the full trade-off analysis.


Architecture Selection

Choosing the right data architecture is the highest-leverage decision in a data platform. Get it right, and everything else follows. Get it wrong, and you spend years working around the mismatch.

Primary Decision Matrix

Your SituationArchitectureCost ProfileComplexityDetailed Reference
Structured analytics, known query patternsData WarehouseMedium (compute + storage)Low-mediumdata-warehouse-architecture
Unstructured data, schema-on-read flexibilityData LakeLow (storage-heavy)Mediumdata-lake-architecture
Both structured and unstructured, ACID neededLakehouseMediumMedium-highlakehouse-architecture
Multiple teams, domain-driven data ownershipData MeshHigh (organizational overhead)Highdata-mesh-architecture
Sub-second latency, event-driven processingStreamingHigh (always-on compute)Highstreaming-architecture
Standard pipeline layering (raw → clean → business)MedallionDepends on storage choiceLowmedallion-architecture

Architecture Decision Flowchart

1. Is your data primarily structured (tables, schemas)?
   ├── Yes → Do you need sub-second processing?
   │         ├── Yes → Streaming architecture
   │         └── No → Data Warehouse (Kimball star schema)
   └── No (or mixed) ↓

2. Do you have unstructured data (images, logs, text)?
   ├── Yes → Do you also need SQL analytics on it?
   │         ├── Yes → Lakehouse (BigLake, Delta Lake, Iceberg)
   │         └── No → Data Lake (GCS zones)
   └── No ↓

3. Do multiple teams need independent data products?
   ├── Yes → Data Mesh (organizational pattern, not a technology)
   └── No ↓

4. Default: Data Warehouse with Medallion layering.

The Medallion Architecture Is Not an Alternative

Medallion (Bronze/Silver/Gold) is a layering pattern, not a competing architecture. You can apply Medallion inside a Data Warehouse, a Data Lake, or a Lakehouse. It defines how data flows through refinement stages. Every architecture in this table benefits from Medallion layering. See medallion-architecture for the layer definitions.

Architecture Combinations (Real-World)

Most production systems combine multiple patterns:

CombinationWhen It WorksExample
Warehouse + MedallionStandard analytical platformSQL Server Bronze/Silver → BigQuery Gold
Lake + WarehouseRaw storage + analytical queriesGCS landing zone → BigQuery analytical layer
Lakehouse + StreamingReal-time analytics on mixed dataPub/Sub → Dataflow → BigLake (Iceberg)
Mesh + WarehouseLarge org with autonomous teamsEach team owns a domain warehouse, federated via BigQuery
Warehouse + StreamingBatch + real-time in one platformBigQuery (batch) + Pub/Sub → Dataflow (real-time)

Build vs Buy Decision Framework

The most consequential decision in engineering is not which technology to use — it is whether to build the capability yourself or buy it from a vendor.

Primary Decision Matrix

FactorBuild (Custom Code)Buy (Managed Service / SaaS)
Cost at low scaleLower (just compute + engineering time)Higher (per-seat or per-unit pricing)
Cost at high scaleHigher (engineering time dominates)Lower (amortized across vendor’s customer base)
Time to valueWeeks to monthsDays to weeks
CustomizationUnlimited (you own the code)Limited to vendor’s API and configuration
Maintenance burdenOn you, forever (upgrades, patches, bugs)On the vendor (you pay for this)
Vendor lock-in riskNoneMedium to high (data gravity, API dependency)
Hiring requirementsNeed specialists who can build and maintainLess specialized team can operate
ReliabilityDepends on your team’s skillUsually high (vendor’s reputation depends on it)
Feature velocityLimited by your team’s bandwidthVendor ships features for all customers
Knowledge retentionIn your codebase (risk if key people leave)In vendor’s product (survives team changes)

The Build vs Buy Rule

The Core Principle (Reis & Housley)

“Build what differentiates your business. Buy everything else.”

If a capability is your competitive advantage — the unique thing that makes your product better than alternatives — build it. You need full control, deep customization, and the ability to iterate faster than any vendor can. For everything else, buy. Your time is better spent on differentiation than on reinventing infrastructure that thousands of other companies also need.

Applied Examples

CapabilityBuild or BuySpecific ChoiceReasoning
OrchestrationBuy (managed) or Semi-buildCloud Composer or self-hosted AirflowOrchestration is not your competitive advantage; reliability is table stakes
MonitoringBuyDatadog or GCP Cloud MonitoringBuilding an observability platform is a full-time job for a team. See datadog-architecture-overview
Data warehouseBuyBigQueryNever build your own query engine. See querying-and-cost-optimization
Custom scoring modelBuildPython + SQL Server + BigQueryThis IS your competitive advantage — full control required
ETL frameworkBuy for standard sources, Build for customdbt (transforms), Fivetran (ingestion), custom Python (APIs)Standard connectors are commoditized; custom sources need custom code
DashboardsBuy or Semi-buildLooker (buy) or Blazor (semi-build)Depends on customization needs and existing skills
CI/CDBuyGitHub ActionsCI/CD is infrastructure, not differentiation. See github-actions-ci-cd
Secret managementBuyGCP Secret ManagerNever roll your own cryptography or secret storage
Log aggregationBuyDatadog Logs or Cloud LoggingBuilding log infrastructure is not your job. See datadog-log-management
Data quality checksSemi-builddbt tests + custom Python assertionsdbt handles standard checks; custom business rules need custom code
Schema registryBuyConfluent Schema Registry or BigQuery schemaUnless you have unique schema evolution needs
Feature storeBuild (usually)BigQuery + FirestoreML feature stores are domain-specific; generic tools rarely fit

The Semi-Build Pattern

Many decisions are not pure build or pure buy. The “semi-build” pattern uses a managed foundation with custom logic on top:

Foundation (Buy)Custom Layer (Build)Result
Airflow (orchestration engine)Custom DAGs, operators, pluginsManaged scheduling + custom pipeline logic
dbt (transform framework)Custom SQL models, macros, testsManaged workflow + custom business transforms
BigQuery (query engine)Custom UDFs, stored procedures, viewsManaged compute + custom analytical logic
Docker (container runtime)Custom Dockerfiles, entrypointsManaged isolation + custom application packaging
GitHub Actions (CI engine)Custom workflows, composite actionsManaged runners + custom deployment logic
Terraform (IaC engine)Custom modules, providersManaged state + custom infrastructure patterns

The Vendor Lock-In Test

Before choosing “buy,” ask: “What happens if this vendor doubles their price or shuts down?” If the answer is “we rewrite everything,” the lock-in risk is high. Mitigate by:

  1. Using open standards (SQL, Parquet, OpenTelemetry) where possible
  2. Keeping raw data in a format you control (GCS + Parquet, not only in the vendor’s proprietary format)
  3. Abstracting vendor-specific APIs behind your own interfaces See serialization-formats for portable data format choices.

Monitoring and Observability Selection

What to Monitor With What

What You MonitorToolWhy
SQL Server performance (wait stats, queries)Datadog (SQL Server integration)Deep metrics, custom queries, alerting. See datadog-sql-server-integration
Airflow DAG health (task duration, failures)Datadog (Airflow integration)Correlate DAG failures with infrastructure metrics. See datadog-airflow-observability
GCP resource usage and billingCloud MonitoringNative, free for GCP metrics, tight IAM integration
Application logs (structured)Cloud Logging or Datadog LogsCloud Logging is free tier; Datadog for cross-platform. See cloud-logging
Pipeline SLA complianceCloud Monitoring + custom metricsTrack pipeline freshness against SLAs. See gcp-pipeline-health-and-sla
Infrastructure dashboardsDatadogUnified view across SQL Server, Airflow, GCP. See datadog-dashboards
Cost anomaly detectionGCP Billing alerts + Cloud MonitoringCatch runaway queries or forgotten VMs. See finops-cost-optimization
Distributed tracesCloud Trace or Datadog APMTrack requests across services. See datadog-apm-traces
Data lineage and catalogingGCP Data Catalog or dbt docsTrack where data comes from and where it goes. See gcp-data-lineage-and-catalog
Uptime and endpoint healthCloud Monitoring (uptime checks)Synthetic checks on HTTP endpoints. See gcp-cloud-monitoring-deep-dive

The Observability Stack Rule

You need three pillars: metrics (how much), logs (what happened), and traces (where time went). A single tool that covers all three is better than three separate tools. Datadog covers all three but costs money. GCP Cloud Monitoring + Cloud Logging + Cloud Trace covers all three within GCP but lacks cross-platform visibility. Choose based on whether your stack is GCP-only or hybrid. See observability-deep-dive for the full framework.


CI/CD and Deployment Selection

Deployment Strategy by Workload

WorkloadDeployment MethodWhy
Airflow DAGsGit push → sync to DAGs folderDAGs are Python files; deploy = copy to the right directory. See airflow-deployment
Cloud Run services/jobsGitHub Actions → gcloud run deployBuild Docker image, push to Artifact Registry, deploy. See github-actions-ci-cd
Terraform infrastructureGitHub Actions → terraform plan/applyPlan on PR, apply on merge to main
SQL Server schema changesMigration scripts (sequential, idempotent)Version-controlled .sql files. See migration-idempotency-backfills
dbt modelsGitHub Actions → dbt buildTest and deploy SQL transforms. See dbt-transformation-layer
Python packagesGitHub Actions → build + publishpip-installable packages for shared libraries
Docker imagesGitHub Actions → build + push to Artifact RegistrySee image-management for image patterns

Testing Strategy by Layer

LayerTest TypeToolWhen
SQL transformsdbt tests (schema + custom)dbt testOn every PR, before deploy
Python pipeline codeUnit tests (pytest)pytestOn every PR. See python pipeline execution
API endpointsIntegration testspytest + httpxOn every PR
InfrastructureTerraform plan reviewterraform planOn every PR. See plan-apply-destroy
Data qualityRow counts, null checks, uniquenessdbt tests or custom SQLAfter every pipeline run
End-to-end pipelineSmoke test on stagingCustom scriptBefore production deploy
Docker imagesContainer scan + build testTrivy, docker buildOn every PR. See container-lifecycle

Container and Runtime Selection

Docker vs Bare Metal vs Managed Runtime

ScenarioUse ThisWhy
Pipeline jobs with specific dependenciesDocker (Cloud Run Job)Isolated, reproducible, version-pinned. See cloud-run-jobs-vs-services
Long-running stateful serviceDocker on Compute EngineNeed persistent disk + specific OS config
SQL ServerBare metal (on VM)SQL Server licensing and performance tuning need direct OS access
AirflowDocker Compose (on VM)Reproducible setup, easy upgrades. See docker-compose
Quick script executionDirect Python/BashDocker overhead not justified for a 10-second script
Multi-service local devDocker ComposeSpin up DB + app + worker in one command
Production KubernetesGKE AutopilotOnly if you have 10+ services and a dedicated platform team

Docker Decision Rules

When to Dockerize

  • Always Dockerize if the code runs on Cloud Run (it requires a container image)
  • Always Dockerize if the code has complex dependencies (specific Python version + system libraries)
  • Always Dockerize if multiple engineers need to run the same environment
  • Skip Docker for simple scripts with no special dependencies
  • Skip Docker for SQL-only operations (dbt, T-SQL scripts)
  • Skip Docker for one-off gcloud CLI operations

See container-lifecycle for image building and image-management for registry patterns.


Version Control and Collaboration Selection

Git Workflow by Team Size

Team SizeWorkflowWhy
Solo developerTrunk-based (commit to main)No merge overhead, fast iteration
2-3 developersFeature branches + PRsCode review without ceremony. See pull-requests-and-code-review
4+ developersFeature branches + required reviewsEnforce standards, catch issues early
Multiple teamsFeature branches + CODEOWNERSAutomatic reviewer assignment

See git-daily-workflow for daily patterns and git-branching-and-merging for branch strategies.


Cost Optimization Decision Framework

When choosing between technologies, cost is a dimension — not the only dimension, but one that compounds over time.

Cost Comparison by Workload Pattern

Workload PatternCheap OptionExpensive OptionThreshold
Compute that runs <2 hours/dayCloud Run JobCompute Engine VM (24/7)VM wastes 91% of uptime cost
Compute that runs 24/7Compute Engine VM (committed use)Cloud Run (per-request pricing)Cloud Run per-request cost exceeds VM at ~50% utilization
Analytics on <1 TBSQL Server (existing VM)BigQuery (per-query)If SQL Server is already running, marginal cost is zero
Analytics on >10 TBBigQuery (flat-rate or per-TB)SQL Server (massive VM)SQL Server VM cost explodes at scale
Storage (frequently accessed)GCS StandardBigQuery storageGCS is 0.020/GB/mo (same)
Storage (rarely accessed)GCS Coldline ($0.004/GB/mo)Any active storage tier5x savings for archival data
Orchestration (<10 DAGs)Self-hosted Airflow (~$75/mo)Cloud Composer (~$350/mo)4.5x cost difference for same capability
Orchestration (50+ DAGs)Cloud Composer (~$500/mo)Self-hosted Airflow (multiple VMs + ops time)Ops time exceeds Composer premium
MonitoringCloud Monitoring (free tier)Datadog (~$23/host/mo)Datadog justified when managing 5+ services

The FinOps Decision Rule

Cost optimization is not about choosing the cheapest option — it is about choosing the option with the best cost-to-value ratio for your specific workload pattern. A 75/month self-hosted Airflow that requires 10 hours/month of maintenance. See finops-cost-optimization for detailed cost analysis.

Reserved vs On-Demand Decisions

ResourceReserved (Committed Use)On-DemandDecision Rule
Compute Engine VMs1-year CUD: 37% discount, 3-year: 55%Full priceIf VM runs 24/7 for >6 months, commit
BigQueryFlat-rate slotsPer-TB scannedIf monthly scan >10 TB consistently, evaluate flat-rate
Cloud SQLPer instance hourUse smallest instance that meets latency SLA
BigtablePer node hourNodes are expensive; auto-scaling helps but has minimums

Serialization Format Selection

When data moves between systems, the format matters for performance, compatibility, and cost.

ScenarioFormatWhyDetailed Reference
Data warehouse staging (analytics)ParquetColumnar, compressed, partition-friendlyserialization-formats
API responsesJSONUniversal, human-readable, every language parses itrest-api-design-and-consumption
High-throughput service communicationProtocol BuffersBinary, schema-enforced, smallest wire sizegrpc-for-data-pipelines
Configuration filesYAML or JSONHuman-readable, widely supported
Log data (append-heavy)NDJSON (newline-delimited JSON)One record per line, streamable, grep-friendly
Small CSV exchangesCSVUniversal, Excel-compatibledata formats and serialization
Large dataset archivalParquet + Snappy compressionBest compression-to-read-speed ratioparquet files
ML training dataParquet or TFRecordParquet for tabular, TFRecord for TensorFlow
Schema evolution requiredAvro or ParquetBoth support schema evolutionserialization-formats

The Parquet Default

When moving data between pipeline stages, default to Parquet. It is columnar (efficient for analytical queries), compressed (cheap to store), self-describing (schema embedded), and supported by every major tool (BigQuery, Spark, pandas, Polars, dbt). The only exceptions are when you need human readability (use JSON) or streaming (use NDJSON). See parquet files for detailed usage patterns.


Technology Selection Anti-Patterns

Common mistakes when selecting technology — and the correction:

Anti-PatternWhy It FailsCorrection
Resume-driven developmentChoosing tech because it looks good on a resume, not because it fits the problemChoose boring technology that solves the problem. Excitement fades; maintenance does not.
Premature optimizationChoosing Bigtable or Kafka “for scale” when you have 100 MB of dataStart with the simplest option. Migrate when you hit a real limit, not an imagined one.
Shiny object syndromeAdopting every new tool (Pulumi over Terraform, Dagster over Airflow)New tools must be 10x better to justify the switching cost. Incremental improvements do not clear the bar.
Not-invented-hereBuilding custom solutions for solved problems (custom orchestrator, custom monitoring)Build only what differentiates. Buy everything else.
Vendor worshipAssuming the vendor’s recommended architecture is right for your scaleVendor architectures are designed for their largest customers. Evaluate at your actual scale.
Ignoring migration costChoosing the “best” database without accounting for the 6-month migration effortThe second-best technology that you already run often beats the best technology that requires migration.
Over-engineeringKubernetes for 3 services, Data Mesh for a 2-person team, Kafka for 100 events/dayMatch the solution complexity to the problem complexity.
Under-engineeringCron + bash for 50 interconnected pipelines with retry requirementsRecognize when you have outgrown a simple tool and it is time to escalate.
Single-tool thinkingTrying to do everything in Python (including what SQL does better) or everything in SQL (including what Python does better)Use each tool for what it does best. Polyglot engineering is a strength.
Ignoring team skillsChoosing Go because it is “better for APIs” when your entire team writes PythonThe best technology is the one your team can operate reliably. Skill gaps take months to close.

Quick-Reference: “Which Tool For…” Lookup

For rapid lookup when you just need the answer:

“I need to…”

TaskAnswerNote Reference
…transform data in a databaseSQLsql-fundamentals
…call an API and load resultsPythonrest-api-design-and-consumption
…schedule a daily jobCloud Scheduler + Cloud Run Jobgcp-scheduling
…orchestrate 10+ dependent jobsAirflowairflow-core-concepts
…provision a VMTerraformcompute
…do a quick one-off query in BigQuerybq CLI or Consolequerying-and-cost-optimization
…move files between serversBash (rsync/scp)vm-ssh-and-file-transfer
…build a dashboardBlazor (C#) or Looker
…store pipeline stateFirestorefirestore-data-model-and-operations
…send data between servicesPub/Subpubsub-messaging
…monitor SQL ServerDatadogdatadog-sql-server-integration
…version-control infrastructureTerraform + Gitstate-management
…containerize a Python pipelineDockercontainer-lifecycle
…test data qualitydbt testsdbt-transformation-layer
…parse a log file quicklyBash (grep/awk)grep-and-pattern-matching
…manage SQL Server backupsT-SQL + PowerShellbackup-types-and-strategy
…set up CI/CD for a pipelineGitHub Actionsgithub-actions-ci-cd
…encrypt data at restTDE (SQL Server) or GCS encryptiontde-encryption
…manage service accountsTerraform + IAMservice-accounts-and-iam
…explore a new GCP serviceConsole (UI), then translate to Terraformgcp-apis-and-services
…handle schema migrationsIdempotent SQL scriptsmigration-idempotency-backfills
…choose a data format for transferParquetserialization-formats
…set up Airflow on a VMDocker Composeairflow-deployment
…connect Python to SQL Serverpyodbc or SQLAlchemydatabase connections
…optimize BigQuery costsPartitioning + clustering + avoid SELECT *querying-and-cost-optimization
…debug slow SQL Server queriesWait stats + execution planswait-stats-analysis
…design a data model for BIStar schema (Kimball)dimensional-modeling
…audit who has access to whatIAM policy reviewservice-accounts-and-iam
…set up alerting for pipeline failuresDatadog monitors or Cloud Alertingdatadog-alerting
…process streaming dataDataflow (Apache Beam)streaming-architecture

Architecture and modeling

Comparison references

Implementation details

Observability and operations