Declarative data pipelines: we swapped PySpark for YAML

Here is the case for declarative data pipelines. The standard advice for years has been to answer every data problem with more PySpark and more custom Python boilerplate. What that buys you is a bottleneck, where an analyst waits weeks on an engineer to ship a single metric.

I have watched this play out on plenty of projects. A “simple” data request comes in, and three weeks later you are still tuning Spark resource allocation and untangling Airflow DAG dependencies. Moving data around should not require a background in distributed systems.

Where the PySpark bottleneck comes from

On an architecture review a while back, a client was paying $40k a month for a cluster whose main job was counting Monthly Active Users (MAU). Adding one event source meant a developer wrote a custom PySpark script, waited on code review, then hoped no “Out of Memory” error fired at 3 AM. That does not hold up over time. If any of it sounds familiar, it may be worth rethinking how you are scaling real-time data pipelines.

The problem is not the code, it is ownership. Logic trapped in Python locks the analyst out of production, and the analyst is the person who understands the business requirements best. What you want instead are declarative data pipelines, where the config file doubles as the documentation.

The stack: dlt, dbt and Trino

We swapped the procedural code for a declarative setup built on three tools. Anyone who knows SQL and enough YAML to edit a config file can ship a production pipeline in about 24 hours.

  • dlt (data load tool): pulls data out of APIs and databases from a YAML config.
  • dbt (data build tool): runs on top of Trino and does the transformations in plain SQL.
  • Airflow + Cosmos: generates the DAGs from your dbt project and runs them.

Step 1: ingestion in YAML

Instead of a 200-line Python script that handles pagination and retries, the source goes in a dlt.yaml file. Ingesting a billing API looks like this:

source:
  type: rest_api
  client:
    base_url: "https://api.example.com"
    auth:
      type: bearer
      token: billing-token
  resources:
    - name: charges_raw
      endpoint:
        path: /data/charges
        method: POST
      write_disposition: replace

Step 2: transformation in SQL

Once the data has landed in storage, dbt models transform it. Trino supports federated queries, so you can join across different stores without moving anything first, which keeps the transformation logic readable. Bear in mind that optimization matters in SQL too.

-- int_mau_events.sql
{{ config(materialized='table') }}

SELECT 
    customer_id, 
    'visit' AS event_type, 
    endpoint
FROM {{ source('raw', 'visits') }}
WHERE customer_id IS NOT NULL

Where this approach falls short

Trino’s fault tolerance does not match Spark’s on terabyte-scale shuffles. Lose a worker mid-query and the whole query can fail. dlt’s support for some formats, Delta among them, is also still maturing. So do not migrate everything on principle. Keep PySpark for the 10% of jobs that need heavy custom Python logic SQL cannot reach.

If pipeline work like this is eating your dev hours, I can take it off your hands. I have been wrestling with WordPress and awkward backend integrations since the 4.x days.

Scaling comes down to ownership

The declarative model moved pipeline building off the engineering team and onto the analysts who wanted the data. Delivery went from weeks to a single day, and nobody had to write boilerplate to get there.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.