Padrões de Engenharia de Dados e Pipelines ETL para Equipes Modernas
From batch processing to real-time streaming, learn the patterns that power modern data platforms and how to build pipelines that scale.
The data engineering landscape in 2026 looks nothing like it did five years ago. The lines between data warehousing, data lakes, and streaming platforms have blurred into unified architectures. Teams no longer choose between batch and streaming — they build pipelines that handle both. The tools have matured, the patterns have crystallized, and the expectations for data reliability have never been higher.
Yet despite the maturity of the ecosystem, most data teams still struggle with the same fundamental challenges: pipelines that break silently, schemas that drift apart, and data quality issues that surface weeks after the damage is done. This guide covers the patterns that separate reliable data platforms from fragile ones — from processing paradigms to orchestration, transformation, testing, and warehouse design.
Whether you are building a greenfield data platform or migrating legacy ETL into a modern stack, these patterns will help you design pipelines that are testable, observable, and maintainable as your data volume grows from gigabytes to petabytes.
Batch vs. Streaming Processing
The debate between batch and streaming processing is no longer about which paradigm is better — it is about understanding when each applies and how to combine them. Batch processing processes finite data sets on a schedule, delivering predictable resource usage and straightforward reprocessing semantics. Streaming processes data continuously as it arrives, enabling sub-second latency at the cost of operational complexity.
Batch remains the dominant pattern for most enterprise workloads. It is simpler to debug, easier to test, and more forgiving of data quality issues. If your business generates nightly reports, batch processing using tools like Spark or dbt scheduled through Airflow is often the right choice. The key insight is that batch gives you a natural checkpoint — every run is a self-contained unit of work that can be validated, rolled back, or replayed.
When Streaming Becomes Necessary
Streaming processing becomes essential when latency requirements exceed what batch can deliver. Fraud detection, real-time personalization, operational dashboards, and event-driven microservices all demand sub-second processing. Tools like Apache Flink, Kafka Streams, and RisingWave provide exactly-once semantics that rival batch processing in correctness, but they require careful handling of state, watermarks, and checkpointing.
The pragmatic approach in 2026 is the lambda architecture revisited — a unified pipeline that uses streaming for real-time views and batch for historical correctness. But unlike the original lambda architecture, modern tools like Apache Spark Structured Streaming and Flink allow you to write a single codebase that runs in both batch and streaming modes, dramatically reducing the maintenance burden of maintaining two parallel pipelines.
"The question is not whether to use batch or streaming, but how to make them converge. A pipeline that cannot produce the same result from both modes is a pipeline you cannot trust."
The Data Lakehouse Architecture
The lakehouse architecture emerged as a response to the limitations of both data lakes and data warehouses. Data lakes offered cheap storage and schema-on-read flexibility but suffered from poor data quality, lack of ACID transactions, and no SQL access. Data warehouses provided reliability and performance but imposed rigid schemas and expensive storage costs. The lakehouse combines the best of both: cheap object storage with warehouse-grade ACID transactions and SQL access.
The foundation of the lakehouse is the open table format. Delta Lake, Apache Iceberg, and Apache Hudi each provide ACID transactions, time travel, schema evolution, and efficient upserts on top of Parquet files stored in object storage. These formats have become the standard way to organize data lakes, replacing the ad-hoc directory structures that made traditional data lakes unreliable.
Delta Lake vs. Apache Iceberg
Delta Lake, developed by Databricks, provides the tightest integration with Spark and MLflow. Its transaction log format is mature, its merge operations are battle-tested, and its runtime ensures snapshot isolation for concurrent readers and writers. Delta Lake also pioneered features like generated columns, liquid clustering, and deletion vectors that make it particularly strong for Lakehouse architectures within Databricks ecosystems.
Apache Iceberg, originally developed at Netflix and now a top-level Apache project, has gained significant traction due to its engine-agnostic design. Iceberg tables can be read and written by Spark, Flink, Trino, DuckDB, and Snowflake alike. Its REST catalog specification has become a de facto standard for table management across the ecosystem. If your platform uses multiple query engines, Iceberg provides the best interoperability today.
Both formats support partition evolution, hidden partitioning, and file-level statistics for query optimization. Choosing between them depends on your primary compute engine and catalog requirements rather than any fundamental capability gap.
Apache Spark for Large-Scale Transformations
Apache Spark remains the most widely used compute engine for large-scale data transformations. Its ability to process both batch and streaming workloads in a unified API, combined with a rich ecosystem of connectors and ML libraries, makes it the default choice for data engineering teams. The key to using Spark effectively is understanding how its execution model maps to your transformation logic.
A typical Spark ETL pipeline reads from a source, applies transformations, and writes to a target table. The most common anti-pattern is treating Spark like a single-node Pandas script — calling collect() too early, ignoring partitioning, and writing too many small files. Modern Spark best practices emphasize cost-based optimization, adaptive query execution, and proper file sizing through dynamic partition writes.
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, window, avg, count, when
from pyspark.sql.types import StructType, StructField, StringType, TimestampType, DoubleType
spark = SparkSession.builder \
.appName("order_metrics_etl") \
.config("spark.sql.adaptive.enabled", "true") \
.config("spark.sql.adaptive.coalescePartitions.enabled", "true") \
.getOrCreate()
schema = StructType([
StructField("order_id", StringType(), nullable=False),
StructField("user_id", StringType(), nullable=False),
StructField("amount", DoubleType(), nullable=False),
StructField("status", StringType(), nullable=False),
StructField("event_time", TimestampType(), nullable=False),
])
raw_df = spark.readStream \
.schema(schema) \
.option("maxFilesPerTrigger", 10) \
.json("s3://landing/orders/")
aggregated = raw_df \
.withWatermark("event_time", "10 minutes") \
.groupBy(
window(col("event_time"), "5 minutes"),
col("status"),
) \
.agg(
count("order_id").alias("order_count"),
avg("amount").alias("avg_order_value"),
)
query = aggregated.writeStream \
.format("delta") \
.outputMode("append") \
.option("checkpointLocation", "s3://checkpoints/order_metrics") \
.partitionBy("status") \
.trigger(processingTime="1 minute") \
.table("analytics.order_metrics")This pattern demonstrates several best practices: structured streaming with watermarks for late data handling, adaptive query execution enabled for automatic optimization, and checkpointing for exactly-once guarantees. The write path uses Delta Lake to ensure ACID compliance and efficient partitioning by status.
Performance Tuning and Cost Optimization
The most expensive mistake in Spark is over-provisioning. Start with a modest cluster and use the Spark UI to understand your bottleneck. If your job is CPU-bound, increase cores. If it is I/O-bound, optimize file sizes and use column pruning. If it experiences shuffle skew, add salting or use skew join hints. Modern Spark 4.x includes significant improvements to join performance, especially for semi-structured data types like variant and struct.
- Enable adaptive query execution and dynamic partition pruning for automatic optimization
- Use Delta Lake OPTIMIZE and ZORDER commands to maintain file sizes between 128 MB and 1 GB
- Prefer PySpark DataFrame API over RDDs for most ETL workloads — the optimizer handles Catalyst rules
- Configure shuffle partitions based on data size, not cluster size — start with the default and adjust
- Use column pruning and predicate pushdown to minimize I/O when reading source tables
Streaming with Kafka and Flink
Apache Kafka has become the backbone of event-driven data platforms. It acts as the durable log that decouples producers from consumers, enabling teams to build streaming pipelines without tight coupling between services. In 2026, Kafka is no longer just a message broker — it is a storage system with tiered storage, exactly-once semantics, and a rich ecosystem of connectors via Kafka Connect.
The fundamental Kafka pattern is the event-driven pipeline: services produce events to topics, stream processors consume and transform those events, and the results are written to sink systems like data warehouses, caches, or search indexes. The key design decision is how to model your topics — by entity type, by event type, or by domain. Most successful architectures use a domain-event pattern where each topic represents a specific business event that has occurred.
Stream Processing with Flink
Apache Flink is the most capable stream processing engine available today. It provides true event-time processing, exactly-once state semantics, savepoints for version upgrades, and a SQL interface that makes streaming accessible to analysts. Flink's checkpointing mechanism periodically saves the state of all operators to a durable store, enabling recovery from failures with no data loss and minimal reprocessing.
The most common Flink pattern is the ETL stream: read from Kafka, enrich with a side input or lookup table, transform, and write to a sink. Flink's SQL client makes this accessible without writing Java or Scala code. For example, a simple streaming enrichment that joins an order stream with a customer dimension table can be expressed as a streaming SQL query with a lookup join, running with exactly-once semantics and sub-second latency.
Managing Kafka at Scale
Kafka clusters require careful capacity planning. Factor in replication factor (three is standard for production), retention policies (time-based for most use cases, size-based for compliance), and partition count (aim for ten partitions per broker for balanced load). Monitor consumer lag as your primary operational signal — growing lag indicates a downstream processing bottleneck. Use Kafka MirrorMaker or Confluent Cluster Linking for cross-region replication.
- Model topics around business events, not table rows — events are immutable facts that can be replayed
- Use Avro or Protobuf with Schema Registry to enforce schema compatibility at the producer level
- Set retention based on your replay requirements — 7 days is typical, longer for compliance
- Monitor consumer group lag as your primary health metric for streaming pipelines
- Use Kafka Connect for source and sink connectors to reduce custom integration code
dbt for Data Transformations
dbt has transformed how data teams approach transformations. Instead of writing SQL in a notebook or a GUI, dbt treats transformations as software — version-controlled, tested, documented, and deployed through CI/CD. The core model is simple: you write SELECT statements in SQL or Python, and dbt handles the boilerplate of CREATE TABLE, INSERT, MERGE, and DROP operations, along with dependency resolution and incremental loading.
The dbt approach enforces a clean separation of concerns. Raw data lands in a staging layer where it is cleaned and typed. Staging models feed into intermediate models that perform business logic and deduplication. Intermediate models feed into mart models that are exposed to end users in BI tools. Each layer has a clear responsibility, and the lineage graph generated by dbt makes dependencies visible across the entire project.
-- models/marts/finance/daily_revenue_by_product.sql
{{
config(
materialized='incremental',
incremental_strategy='merge',
unique_key=['order_date', 'product_id'],
on_schema_change='fail',
partition_by={
'field': 'order_date',
'data_type': 'date',
'granularity': 'day'
}
)
}}
with orders as (
select * from {{ ref('stg_orders') }}
{% if is_incremental() %}
where order_date >= date_sub(current_date, interval 3 day)
{% endif %}
),
order_items as (
select * from {{ ref('int_order_items') }}
),
products as (
select * from {{ ref('dim_products') }}
),
revenue as (
select
o.order_date,
p.product_id,
p.product_name,
p.category,
count(distinct o.order_id) as order_count,
count(oi.item_id) as units_sold,
sum(oi.revenue) as gross_revenue,
sum(oi.revenue * (1 - coalesce(o.discount_pct, 0))) as net_revenue
from orders o
inner join order_items oi on o.order_id = oi.order_id
inner join products p on oi.product_id = p.product_id
group by 1, 2, 3, 4
)
select * from revenueThis example shows a production-grade incremental model. It uses a merge strategy for upserts, a three-day lookback window for incremental runs, and explicit column-level lineage through ref() calls. The on_schema_change fail setting prevents silent column drift. The partition by configuration optimizes query performance on the target date column.
Testing and Documentation in dbt
dbt's testing framework is one of its strongest features. Singular tests are custom SQL queries that return failing rows. Generic tests are reusable assertions — not null, unique, accepted values, relationships, and custom generic tests defined in your project. A robust dbt project runs hundreds of tests on every deployment, catching data quality issues before they reach production dashboards.
- Add not_null and unique tests to every primary key column in every model
- Use relationship tests to enforce referential integrity between staging and dimension models
- Write custom generic tests for business rules — no negative prices, no future dates, no orphaned records
- Use dbt-docs to auto-generate project documentation with lineage graphs
- Run tests in CI on every pull request and block merges when tests fail
"A dbt model without a test is just a query. Testing transforms data from an artifact into an asset that your team can trust."
Orchestration with Airflow and Dagster
Orchestration is the nervous system of a data platform. It manages dependencies between tasks, handles retries and failures, and provides visibility into pipeline health. Apache Airflow has been the dominant orchestrator for years, but Dagster has emerged as a strong alternative with a focus on software-defined assets, type safety, and developer experience.
The core pattern for orchestration is the DAG — a directed acyclic graph of tasks with explicit dependencies. Each task should represent a single unit of work: run a Spark job, execute a dbt model, send a notification, or copy a file. The DAG structure makes dependencies visible and ensures that downstream tasks only run when their upstream dependencies succeed.
Airflow Best Practices
Airflow's mature ecosystem includes operators for virtually every data tool — SparkSubmitOperator, KubernetesPodOperator, EmrAddStepsOperator, and the popular BashOperator for wrapping Python scripts. The key to maintainable Airflow is keeping DAGs thin. Business logic belongs in your transformation code, not in Airflow tasks. Each task should be a lightweight call to an external tool, with Airflow handling only orchestration and observability.
Use TaskFlow API for passing data between tasks in a type-safe way. Set up retries with exponential backoff for transient failures — network timeouts, resource contention, API rate limits. Configure SLAs on critical tasks so Airflow alerts when a DAG is about to miss its expected completion time. Use pools and priority weights to prevent resource starvation across concurrent DAGs.
Dagster's Software-Defined Asset Approach
Dagster shifts the mental model from tasks to assets. Instead of defining a DAG of abstract tasks, you define the data assets your pipeline produces — tables, files, metrics — and the computations that produce them. Dagster automatically constructs the asset graph and determines what needs to run when upstream data changes. This asset-centric model makes it easier to reason about data lineage and reduces the boilerplate of traditional DAG definitions.
Dagster also provides first-class testing support with its solid concept, a type system for runtime data validation, and a Dagit UI that shows not just task status but the actual data passing through each asset. For teams adopting software engineering best practices in data, Dagster's approach of treating pipelines as typed, testable software is compelling.
Data Quality Testing and Monitoring
Data quality testing is the most underequipped area in most data platforms. Engineering teams would never deploy application code without unit tests, yet the same teams routinely deploy pipeline changes without any data validation. The consequences are predictable — corrupted dashboards, incorrect ML model inputs, and a slow erosion of trust in the data platform.
A data quality strategy needs two layers: testing at deployment time and monitoring at runtime. Deployment-time testing catches issues before they reach production. Runtime monitoring catches issues that slip through — schema changes from upstream sources, unexpected null rates, distribution drift. Great Expectations, Soda, and dbt tests are the most common tools for deployment testing. For runtime monitoring, tools like Monte Carlo, Bigeye, and Anomalo provide automated anomaly detection.
Building a Data Quality Framework
Start with the critical dimensions: completeness (are all expected rows present?), uniqueness (are primary keys truly unique?), freshness (is the data arriving on time?), and accuracy (do values fall within expected ranges?). Define expectations for each dimension and encode them as tests. A table with 1 million rows of daily sales data should set a row count range expectation — 500,000 to 2 million rows — and alert if the count falls outside that range, which could indicate a missing partition or a duplicate load.
The most effective pattern is the data quality contract. Each data producer — whether an internal team or an external vendor — publishes a schema and quality SLAs. The consuming pipeline validates the contract on every ingestion and rejects data that violates the contract. This forces accountability upstream and prevents bad data from cascading through your platform.
- Run row count checks on every table load — unexpected zeros or spikes indicate a pipeline failure
- Monitor freshness with automated SLA alerts — tables that should update hourly should page if stale
- Track distribution drift — a sudden change in categorical value distribution often signals a source system bug
- Set up weekly data quality reviews where teams review test results and add new expectations
- Create a data quality dashboard visible to all stakeholders — transparency builds trust
"Data quality is not a problem you solve once. It is a discipline you practice every day. The moment you stop paying attention, the data degrades."
Schema Evolution and Migration
Schema evolution is the inevitable challenge of any long-lived data platform. Source systems add columns, rename fields, change data types, and deprecate tables. The data platform must absorb these changes without breaking downstream consumers. The key principle is backward compatibility: new versions of the schema must be readable by old versions of the reader, and forward compatibility is a stretch goal that enables true schema flexibility.
The schema registry pattern is the standard solution. Confluent Schema Registry, combined with Avro or Protobuf, enforces compatibility rules at write time. Producers cannot register a schema change that breaks existing consumers. The supported compatibility levels — BACKWARD, FORWARD, FULL, and NONE — let teams choose the appropriate tradeoff between flexibility and safety. Most production deployments use BACKWARD compatibility as a minimum, meaning new schemas must read old data without errors.
Handling Schema Changes in Lakehouse Formats
Delta Lake and Iceberg both support schema evolution through metadata-only operations. Adding a column is a cheap metadata update that does not rewrite existing data files. Renaming or dropping a column requires more care — old files still use the old column names, and the table format must reconcile them at read time. Both formats support column mapping and default values for new columns, but schema changes should still go through a review process, not be applied automatically.
- Always add nullable columns for new fields — existing rows will have null values, not missing files
- Use schema registries for streaming data to enforce compatibility at write time
- Run schema drift detection as part of your staging layer — log changes and alert on breaking modifications
- Prefer additive schema changes over modifications — new columns are safe, renames require migration
- Document every schema change with a migration log that includes the reason and impact analysis
Data Warehouse Design Patterns
Data warehouse design has evolved significantly. The star schema remains the gold standard for analytical query performance, with fact tables at the center connected to conformed dimension tables. But modern data platforms increasingly use One Big Table designs for specific use cases where query simplicity matters more than storage efficiency. The choice between these patterns depends on your consumption layer and query patterns.
Star Schema: Proven and Predictable
Star schemas organize data into fact tables containing quantitative measures and foreign keys to dimension tables containing descriptive attributes. A sales fact table stores revenue, quantity, and discount, with foreign keys to date, customer, product, and store dimensions. This design enables fast aggregation queries because the fact table is lean and dimensions are denormalized for lookup performance. BI tools like Tableau, Looker, and Metabase generate efficient SQL against star schemas out of the box.
The star schema also enables the drill-anywhere pattern. Users start with a high-level metric like total revenue, then drill into revenue by region, by product category, by customer segment, by time period — any dimension that connects to the fact table. This analytical flexibility is the reason star schemas have dominated data warehousing for decades.
One Big Table: Simple but Costly
The One Big Table pattern pre-joins all dimensions into a single wide table. It simplifies queries — analysts just SELECT from one table without writing JOINs — but at a cost. OBTs are expensive to store and process. Every row duplicates all dimension attributes, increasing storage costs and scan times. Schema changes require full table rewrites. OBTs make sense for consumption patterns that always need all dimensions — for example, serving a pre-aggregated dataset to an ML model or powering a specific dashboard with fixed query patterns.
The pragmatic approach is hybrid: maintain star schemas as your canonical warehouse layer and materialize OBTs for specific high-traffic consumption patterns. Use dbt to define the OBT as a downstream model that depends on the star schema, keeping the logical transformation clear even if the physical storage is denormalized.
Incremental Loading and Slowly Changing Dimensions
Dimensions change over time. A customer moves, a product is recategorized, a supplier changes their payment terms. Type 2 slowly changing dimensions track these changes by inserting new rows with effective date ranges, preserving the historical state of the dimension for accurate point-in-time analysis. Type 1 dimensions overwrite changes, losing history but maintaining simplicity. Type 3 tracks changes in separate columns for limited history. In practice, most dimension tables use Type 2 for critical attributes and Type 1 for non-critical ones.
"A fact without a dimension is a number without context. A dimension without history is a snapshot without memory. Design your warehouse to answer questions about the past, not just the present."
Building Reliable Data Pipelines
Reliability in data pipelines is not about eliminating failures — it is about making failures visible, recoverable, and non-catastrophic. The most reliable pipelines are designed with the expectation that everything will eventually break: source systems will change schemas, networks will timeout, storage will fill up, and bugs will be deployed. The architecture must absorb these failures and recover automatically.
The fundamental reliability pattern is idempotency. A pipeline is idempotent if running it multiple times produces the same result as running it once. Idempotent pipelines can be safely retried, replayed, and backfilled without data duplication or inconsistency. Achieve idempotency by using upsert operations, delete-and-reload patterns, or exactly-once processing semantics in streaming systems.
Observability for Data Pipelines
Data pipeline observability goes beyond orchestration DAG status. You need to know not just whether a pipeline ran, but whether it produced correct results. Monitor row counts through every stage — unexpected drops or spikes indicate problems. Track data freshness as a top-level SLA. Set up lineage-based impact analysis so that when a source system changes, you immediately know which downstream reports and models are affected. Tools like OpenLineage and Marquez provide open-standard lineage tracking across the data stack.
- Make every pipeline idempotent — same input always produces same output regardless of run count
- Implement backfill logic from day one — every pipeline should support historical reprocessing
- Monitor data freshness as a service-level indicator and page when SLIs are breached
- Use OpenLineage to capture column-level lineage across Spark, dbt, Airflow, and your warehouse
- Run dry-run mode on all write operations before committing changes to production tables
Error Handling and Recovery
Design error handling for each failure mode. Transient errors — network timeouts, rate limits, resource contention — should retry with exponential backoff. Data errors — null violations, type mismatches, constraint violations — should quarantine the bad records to a dead-letter table and alert, rather than failing the entire pipeline. Schema errors — unexpected columns, dropped fields — should trigger a review process that determines whether the schema change is safe before the pipeline can proceed.
The dead-letter queue pattern is essential for streaming pipelines. Kafka consumers that encounter deserialization errors or validation failures should write the raw bytes to a dead-letter topic with the error reason as a header. This preserves the data for later analysis without blocking the healthy processing path. A periodic job can review the dead-letter queue, fix the root cause, and replay the failed messages.
Future Trends in Data Engineering
Several trends are reshaping the data engineering landscape as we move through 2026. The first is the rise of the data catalog as the central control plane for data platforms. Tools like Atlan, DataHub, and Apache Atlas have evolved from metadata stores into active governance platforms that enforce policies, manage access, and power discovery. The catalog is becoming the single source of truth for what data exists, where it comes from, who uses it, and how it should be treated.
The second trend is the convergence of data engineering and machine learning engineering. Feature stores like Feast and Tecton have bridged the gap between analytical and ML data, allowing the same pipeline to serve both dashboards and model inference. dbt is increasingly used for ML feature transformations, and Spark's MLflow integration makes it natural to combine ETL and training in the same codebase.
The third trend is the commoditization of streaming through serverless platforms. Confluent Cloud, Redpanda Serverless, and AWS MSK Serverless are removing the operational burden of managing Kafka clusters. Flink on Kubernetes with autoscaling makes stream processing as accessible as running a Spark batch job. The barrier to building real-time pipelines has never been lower.
The AI-Assisted Data Engineer
AI is transforming how data engineers work. LLMs accelerate pipeline development — generating dbt models from natural language descriptions, suggesting Spark optimizations, and writing data quality tests automatically. But the role of the data engineer is not diminishing. The ability to understand data semantics, design robust schemas, debug complex performance issues, and build reliable platforms requires human judgment that AI augments but does not replace. The data engineers who thrive will be those who use AI to amplify their craft, not those who rely on it to replace their thinking.
The future of data engineering is a future of higher leverage. Tools handle the mechanical complexity — partitioning, serialization, checkpointing, retries. The human work shifts to design: what data to collect, how to model it, when to optimize for performance versus flexibility, and how to ensure the platform serves the business reliably over years of evolving requirements. The patterns in this guide are the foundation for that work.
"Data engineering in 2026 is not about moving data from point A to point B. It is about building systems that make data trustworthy, discoverable, and usable — at any scale, under any latency requirement, across any team."
The best data platforms are boring. They run reliably, produce consistent results, and require minimal heroics to operate. Boring is the highest compliment you can give a data pipeline. Invest in the patterns that make your platform boring — testing, monitoring, idempotency, clear contracts — and your team will spend less time fighting fires and more time building value from data.
