An end-to-end data engineering project built on the Olist Brazilian E-Commerce dataset and the Frankfurter currency API. The platform ingests, transforms, and serves data through a medallion architecture on Snowflake — orchestrated by Airflow and deployed via GitHub Actions CI/CD.
| Layer | Tool |
|---|---|
| Source (transactional) | Neon Postgres |
| Source (API) | Frankfurter REST API |
| Ingestion | Airbyte Cloud (CDC) |
| Data Lake | AWS S3 (Parquet) |
| Auto-ingest | Snowpipe (SQS trigger) |
| Warehouse | Snowflake |
| Transformation | dbt (incremental models) |
| Orchestration | Apache Airflow (Astro Cloud) |
| CI/CD | GitHub Actions |
| Data Generator | Python + Faker |
Neon Postgres (Olist)
└── Airbyte CDC ──► AWS S3 ──► Snowpipe ──► Bronze
│
Frankfurter API ▼
└── Python Script ──────────────────────────► Bronze
│
▼
dbt Silver
(clean + dedupe)
│
▼
dbt Gold
(business tables)
- Raw data stored as VARIANT (JSON) — append-only, no transforms
- Schemas:
OLIST,FRANKFURTER - Loaded via Snowpipe (Olist) and Python script (exchange rates)
- Preserves full audit history
- Cleaned, typed, deduplicated
- Incremental dbt models with
unique_keyupserts ROW_NUMBER()deduplication handles CDC duplicatesTRY_TO_TIMESTAMP/TRY_TO_NUMBERfor dirty data- Schemas:
COMMERCE,FINANCE
- Business-ready fact and dimension tables
- Multi-table joins, BRL→USD conversion (ASOF JOIN), customer segmentation
- Schemas:
SALES,PRODUCT
| Table | Description |
|---|---|
fact_orders |
Order-level metrics with delivery days |
dim_customers |
Customer segments (High / Mid / Low Value) |
fact_product_performance |
Product tiers by revenue and review score |
fact_revenue_usd |
Order revenue converted BRL → USD via exchange rates |
ecommerce_data_platform/
├── airflow/
│ ├── dags/
│ │ ├── faker_dag.py # Generates fake orders every 30 mins
│ │ ├── frankfurter_dag.py # Daily exchange rate ingestion
│ │ └── dbt_dag.py # Daily dbt run + test
│ └── docker-compose.yaml
├── data_generator/
│ ├── seed_olist.py # Initial Olist data load to Neon
│ ├── faker_generator.py # Continuous fake order generator
│ └── frankfurter_to_snowflake.py # Frankfurter API → Snowflake
├── ecommerce_dbt/
│ ├── models/
│ │ ├── bronze/ # Staging views over raw tables
│ │ ├── silver/ # Incremental cleaned models
│ │ └── gold/ # Business fact + dim tables
│ ├── macros/
│ │ └── generate_schema_name.sql # Custom schema naming
│ └── dbt_project.yml
├── snowflake/
│ ├── 01_setup.sql # Databases, schemas, warehouses
│ ├── 02_roles.sql # RBAC roles and privileges
│ ├── 03_snowpipe_setup.sql # Storage integration, stages, pipes
│ └── 04_frankfurter_setup.sql # Frankfurter schema + table
└── .github/
└── workflows/
├── dbt_ci.yml # PR: dbt compile + test
└── dbt_deploy.yml # Merge to main: dbt run + test
CDC over full refresh Airbyte uses logical replication (WAL) on Neon Postgres. Only changed rows flow downstream — not full table dumps. This keeps Bronze append-only and Silver incremental.
Full refresh | Overwrite for tables without PKs
order_items and order_payments have no primary keys — CDC incremental isn't possible. These use a TRUNCATE + COPY INTO pattern via Snowpipe overwrite.
SCD Type 1 (overwrite, no history) Silver merges on primary key — latest record wins. Chosen for simplicity; in production SCD Type 2 would preserve history for slowly changing dimensions like customer addresses.
ASOF JOIN for exchange rates
Frankfurter has no data for weekends/holidays. Gold uses Snowflake's ASOF JOIN to find the nearest available rate on or before each order date.
Custom generate_schema_name macro
dbt's default behaviour appends the profile schema to the model schema (e.g. COMMERCE_OLIST). A custom macro overrides this to use the schema exactly as defined in dbt_project.yml.
RBAC with least privilege
Three roles: ECOMMERCE_ENGINEER (human dev), ECOMMERCE_DBT_RUNNER (automation), ECOMMERCE_ANALYST (read-only). CI/CD uses ECOMMERCE_DBT_RUNNER.
Three Airflow DAGs deployed on Astro Cloud:
| DAG | Schedule | Purpose |
|---|---|---|
faker_data_generator |
Every 30 mins | Inserts fake orders into Neon → triggers CDC |
frankfurter_ingestion |
Daily 01:00 UTC | Fetches exchange rates → loads to Bronze |
dbt_run |
Daily 02:00 UTC | Runs all dbt models + data quality tests |
- Pull Request →
dbt compile+dbt testrun automatically. Merge is blocked if tests fail. - Merge to main →
dbt run+dbt testdeploy models to Snowflake automatically.
Snowflake credentials are stored as GitHub Secrets — never hardcoded.
dbt tests across all three layers:
not_nullon all primary keysuniqueon all primary keys in Silver and Goldaccepted_valuesonorder_statusandcustomer_segmentrelationships— orders reference valid customers in Silver
- Snowflake account
- Neon Postgres account
- AWS account (S3 bucket in same region as Snowflake)
- Airbyte Cloud account
- Astro Cloud account (for Airflow)
-- Run in order
snowflake/01_setup.sql
snowflake/02_roles.sql
snowflake/03_snowpipe_setup.sql
snowflake/04_frankfurter_setup.sqlCreate a .env file (never commit this):
NEON_DATABASE_URL=postgresql://...
SNOWFLAKE_ACCOUNT=...
SNOWFLAKE_USER=...
SNOWFLAKE_PASSWORD=...
SNOWFLAKE_WAREHOUSE=ECOMMERCE_BRONZE_WH
requirements.txt file is added.
python -m venv .venv
source .venv/bin/activate # Windows: venv\Scripts\activate
pip install dbt-snowflake snowflake-connector-python requests faker python-dotenv psycopg2-binarypython data_generator/seed_olist.py
python data_generator/frankfurter_to_snowflake.pycd ecommerce_dbt
dbt run
dbt testAdded these secrets to the GitHub repo :
SNOWFLAKE_ACCOUNTSNOWFLAKE_USERSNOWFLAKE_PASSWORDSNOWFLAKE_WAREHOUSESNOWFLAKE_DATABASESNOWFLAKE_ROLE
- Olist Brazilian E-Commerce — 100k orders, 2016–2018, 9 tables
- Frankfurter API — Daily BRL/USD exchange rates, 2016–present, free with no API key
I Built this as a portfolio project to demonstrate end-to-end data engineering skills.

