Skip to content
StackPractices
intermediate By Mathias Paulenko

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

DimensionData LakeData Warehouse
Data typesAll (structured, semi, unstructured)Structured only
SchemaSchema-on-readSchema-on-write
UsersData scientists, ML engineers, analystsBusiness analysts, BI tools
Query performanceVariable, optimized for batchFast, optimized for OLAP
CostLower storage, higher computeHigher storage, optimized compute
Data qualityRaw, may be unvalidatedCurated, validated, trusted
ScalePetabyte+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

PatternFlowBest For
ETLExtract → Transform → LoadData warehouses, strict schema requirements
ELTExtract → Load → TransformData 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

FormatTypeUse Case
CSVTextInterchange, human-readable
JSONSemi-structuredAPIs, nested data
ParquetColumnarAnalytical queries, compression
AvroRow-basedStreaming, schema evolution
ORCColumnarHive/Spark workloads
Delta LakeLayerACID 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.