Data Lake vs Data Warehouse — Architecture Guide
A practical guide to Data Lake architecture: structured vs unstructured storage, lakehouse concepts, ETL vs ELT patterns, and when to choose a lake over a warehouse.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
A Data Lake is a centralized storage repository that holds structured, semi-structured, and unstructured data at any scale. Unlike a Data Warehouse, which stores processed, schema-on-write data in rigid tables, a Data Lake stores raw data in its native format with schema applied on read (schema-on-read). This flexibility makes it ideal for machine learning, exploratory analytics, and storing data whose structure is not yet known. However, without governance, lakes can become “data swamps” — disorganized, unsearchable, and unreliable.
When to Use a Data Lake
-
For alternatives, see Lakehouse Architecture — The Best of Both Worlds.
-
You need to store diverse data types: JSON, CSV, Parquet, images, videos, logs
-
Machine learning workloads require raw, unprocessed data
-
Data volume exceeds what traditional databases can handle well
-
You want to defer schema design until data is consumed
-
Historical data must be retained cheaply for future analysis
Data Lake vs Data Warehouse
| Dimension | Data Lake | Data Warehouse |
|---|---|---|
| Data types | All (structured, semi, unstructured) | Structured only |
| Schema | Schema-on-read | Schema-on-write |
| Users | Data scientists, ML engineers, analysts | Business analysts, BI tools |
| Query performance | Variable, optimized for batch | Fast, optimized for OLAP |
| Cost | Lower storage, higher compute | Higher storage, optimized compute |
| Data quality | Raw, may be unvalidated | Curated, validated, trusted |
| Scale | Petabyte+ | Terabyte to Petabyte |
Architecture Layers
Raw Zone (Bronze)
├── Unprocessed data from sources
├── Retained in native format
└── Cheap, long-term storage
Cleaned Zone (Silver)
├── Deduplicated, validated data
├── Basic transformations applied
└── Typed schemas enforced
Curated Zone (Gold)
├── Business-ready aggregates
├── Optimized for query performance
└── Used by BI tools and applications
ETL vs ELT
| Pattern | Flow | Best For |
|---|---|---|
| ETL | Extract → Transform → Load | Data warehouses, strict schema requirements |
| ELT | Extract → Load → Transform | Data lakes, schema-on-read flexibility |
# ELT pattern — load raw, transform on demand
import pandas as pd
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("DataLakeELT").getOrCreate()
# Extract: Read raw JSON logs from S3
raw_df = spark.read.json("s3://datalake/raw/events/2024/01/")
# Load: Store as Parquet in the Silver zone
raw_df.write.parquet("s3://datalake/silver/events/", mode="overwrite")
# Transform: Apply schema and aggregations on read
cleaned_df = spark.read.parquet("s3://datalake/silver/events/")
cleaned_df.createOrReplaceTempView("events")
daily_metrics = spark.sql("""
SELECT
DATE(timestamp) as date,
event_type,
COUNT(*) as event_count,
COUNT(DISTINCT user_id) as unique_users
FROM events
WHERE timestamp >= '2024-01-01'
GROUP BY DATE(timestamp), event_type
""")
daily_metrics.write.parquet("s3://datalake/gold/daily_metrics/")
Storage Formats
| Format | Type | Use Case |
|---|---|---|
| CSV | Text | Interchange, human-readable |
| JSON | Semi-structured | APIs, nested data |
| Parquet | Columnar | Analytical queries, compression |
| Avro | Row-based | Streaming, schema evolution |
| ORC | Columnar | Hive/Spark workloads |
| Delta Lake | Layer | ACID transactions on lakes |
Delta Lake Example
from delta import configure_spark_with_delta_pip
from pyspark.sql import SparkSession
builder = SparkSession.builder.appName("DeltaLakeExample") \
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
spark = configure_spark_with_delta_pip(builder).getOrCreate()
# Write with ACID guarantees
df.write.format("delta").mode("overwrite").save("/datalake/silver/orders")
# Time travel — query as of a specific version
spark.read.format("delta").option("versionAsOf", 5).load("/datalake/silver/orders")
# Schema evolution
df_with_new_column.write.format("delta") \
.mode("append") \
.option("mergeSchema", "true") \
.save("/datalake/silver/orders")
Common Mistakes
- The data swamp — dumping everything without cataloging, governance, or retention policies
- No partitioning strategy — querying unpartitioned data lakes is painfully slow; partition by date and/or region
- Using lakes for OLTP — lakes are for analytics, not transactional workloads
- Ignoring data governance — without metadata catalogs and access controls, lakes become unusable
- Small files problem — writing thousands of tiny files kills query performance; compact regularly
Troubleshooting
- High latency between services: trace the request path. Look for synchronous chains, missing caching, and oversized payloads that cross network boundaries.
- Single point of failure: identify components without redundancy. Add replicas, failover, or circuit breakers before scaling traffic.
- Unexpected coupling between services: review shared databases, libraries, and schemas. Bound contexts should own their data and expose stable interfaces.
- Cost spikes after scaling: right-size instances and use autoscaling with limits. Reserved capacity or spot instances can reduce steady-state spend.
- Difficult to reason about the system: maintain architecture decision records and service dependency maps. Use observability to validate the diagrams.
FAQ
Can I query a Data Lake with SQL? Yes. Query engines like Athena, Presto/Trino, Dremio, and Spark SQL provide SQL interfaces over lake storage.
Is a Data Lake a replacement for a Data Warehouse? Not exactly. Many organizations use both: lakes for raw/ML data, warehouses for curated BI data. Lakehouse architectures (Delta Lake, Iceberg) blur this line.
How do I prevent my lake from becoming a swamp? Implement: (1) a data catalog for discovery, (2) retention and archiving policies, (3) quality checks at ingestion, (4) clear ownership per dataset.
How do I get started with this in an existing project?
Start with a small, isolated part of your codebase. Apply the concepts from this guide to one module or service. Measure the impact, then expand to other areas.
What tools do I need?
The tools mentioned throughout this guide are listed in each section. Most are open-source and widely adopted. Check the related resources for setup instructions.
How do I measure success after implementing this?
Define clear metrics before starting: performance benchmarks, error rates, or maintainability indicators. Compare before and after. Iterate based on the data, not on assumptions.
Advanced Topics
Detailed Scenario: Event Pipeline with Spark and Delta Lake
System: E-commerce analytics platform
Volume: 50M events/day (clicks, views, purchases)
Storage: S3 with Bronze/Silver/Gold zones
Engine: EMR with Spark 3.5
Architecture:
Sources -> Kinesis -> S3 Bronze (raw JSON)
S3 Bronze -> Spark job -> S3 Silver (Parquet + Delta)
S3 Silver -> Spark job -> S3 Gold (aggregates)
S3 Gold -> Athena/Trino -> BI tools
Step 1: Ingest to Bronze
Kinesis Firehose writes raw JSON to s3://lake/bronze/events/dt=2026-07-11/
Partition by date (dt) for efficient queries
No transformation: data exactly as it arrives
Step 2: Bronze to Silver (cleaning)
$ spark-submit --master yarn \\
--num-executors 20 \\
--executor-memory 8g \\
bronze_to_silver.py
# bronze_to_silver.py
raw = spark.read.json("s3://lake/bronze/events/dt=2026-07-11/")
cleaned = raw.filter("user_id is not null") \\
.withColumn("event_time", to_timestamp("timestamp")) \\
.withColumn("event_date", to_date("event_time")) \\
.dropDuplicates(["event_id"])
cleaned.write.format("delta") \\
.mode("append") \\
.partitionBy("event_date") \\
.save("s3://lake/silver/events/")
Step 3: Silver to Gold (aggregates)
silver = spark.read.format("delta").load("s3://lake/silver/events/")
daily = silver.groupBy("event_date", "event_type") \\
.agg(count("*").alias("total_events"),
countDistinct("user_id").alias("unique_users"))
daily.write.format("delta") \\
.mode("overwrite") \\
.option("overwriteSchema", "true") \\
.save("s3://lake/gold/daily_events/")
Step 4: Optimization
# Compact small files weekly
delta_table = DeltaTable.forPath(spark, "s3://lake/silver/events/")
delta_table.optimize().compact(minFileSize="10MB")
# Vacuum: remove old versions (retain 7 days)
delta_table.vacuum(retentionHours=168)
Monitoring:
- CloudWatch: job duration, memory usage, output file count
- Alert if Silver has > 1000 small files (< 1MB)
- Alert if Gold not updated in 24h
- Data quality: Great Expectations validates schema and nulls in Silver
Estimated monthly cost:
S3 storage (50TB): ~$1,150
EMR (20 executors x 4h/day): ~$800
Athena (BI queries): ~$200
Total: ~$2,150/month
How do I handle schema evolution in a Data Lake?
Use formats that support schema evolution: Parquet, Avro, or Delta Lake. With Delta Lake, the mergeSchema option adds new columns without breaking existing reads. For major changes (renaming columns, changing types), use table versioning: events_v1, events_v2. Document the change in the data catalog. Consumers should use column names, not positions, to avoid breakage.
When should I compact files in the lake?
Compact when the number of small files (< 10MB) exceeds 1000 per partition. Small files degrade performance because Spark/Trino must open each file individually. Run OPTIMIZE weekly for Silver tables and monthly for Gold tables. After compacting, run VACUUM to remove old versions and reclaim storage.
End of document. Review and update quarterly.
Common Production Pitfalls
- Treating the guide as a checklist to complete once rather than a practice to evolve.
- Adopting every recommendation at once instead of starting with one measured change.
- Skipping the maturity assessment and forcing advanced practices on an unprepared team.
- Not updating runbooks and on-call expectations as new practices are introduced.
- Ignoring real incident data when prioritizing which parts of the guide to apply first.
- Failing to assign an owner who reviews decisions quarterly.
- Copying examples without adapting them to the team’s actual tooling and constraints.
- Forgetting to measure outcomes before adding the next improvement.
Related Resources
Lakehouse Architecture — The Best of Both Worlds
A practical guide to Lakehouse architecture: combining data lake storage flexibility with data warehouse reliability using open table formats like Delta Lake, Apache Iceberg, and Hudi.
GuideData Mesh Architecture — Decentralized Data Ownership
A practical guide to Data Mesh: decentralizing data ownership to domain teams, treating data as a product, and enabling self-serve data infrastructure.
GuideDatabase Sharding: Horizontal Partitioning in Practice
A practical guide to database sharding: choosing shard keys, routing queries, rebalancing data, and avoiding common pitfalls when scaling beyond a single database node.