Week 15 - Final Project

Project Ideas and Inspiration

Cross-Track Collaboration

The Project Architecture

Ingestion, ACA, and CI/CD

dbt models

Publish to Postgres

Running the Pipeline Locally

Orchestrating with Airflow

Promoting to Production

Secrets & Variables


Optional: Failure Alerting

Optional: Enriching with an LLM

Optional: The Health Page

Optional: Inbound Sync

Team Timelines

Gotchas & Pitfalls

Frequently Asked Questions

Glossary: Week 15

Career relevance: Week 15

Optional: Inbound Sync

This page covers two sync directions between your pipeline and the application database:

Read the outbound section when you wire the publish task in your DAG. Read the inbound sections only after outbound publish works in prod.

What a good outbound sync looks like

Outbound sync is required for the demo and a passing grade. Users read what it writes, so this task is more demanding than inbound copy.

It runs in your DAG after dbt build and tests on the mart the backend reads.

<aside> 📘 Recap from Week 10: tests on the mart the backend reads are the same generic tests from dbt Tests. A published table with no tests on its keys is the most common demo failure.

</aside>

Five properties make the difference between a sync that survives a demo and one that does not:

It publishes atomically. Load into a staging table, then staging table swap (load to staging, then swap in one transaction) inside a single transaction. Someone refreshing the page during your sync must see either yesterday's data or today's, never an empty table.

https://gist.githack.com/lassebenni/e2195ff7ffc5dfcbff3fe9028fbd2f90/raw/week_15__staging_table_swap_visual.html

It is safe to re-run. Running it twice must leave the same number of rows. Airflow retries tasks, so this is not optional.

It runs on a schedule. After dbt build, with retries, unattended. If the only green run happened because someone pressed a button ten minutes before the demo, it does not count.

It fails loudly. A failed sync must fail the task and leave the previous good data in place. The worst outcome is a green pipeline serving stale numbers, because nobody investigates a green pipeline.

It is sized honestly. Replacing the whole table each run is simple and correct while your marts are small. Say in your README how many rows you handle and why that choice fits. When it stops fitting, you will know, because the run time will tell you.

<aside> 💡 The step from your platform into the application database has a name in industry: reverse ETL. Products exist that do only this. Saying that in an interview, and explaining why you built it yourself, is worth more than the code.

</aside>

Inbound sync (optional)

Read this part after your team has a real external source ingested and a full end-to-end pipeline green in production: external fetch, dbt marts, outbound publish to analytics, and a scheduled Airflow run without anyone pressing a button.

Until that bar is met, do not start inbound sync. Teams that copy app tables before outbound publish works spend weeks debugging two directions at once.

By the end of the inbound sections, you should know when inbound sync is worth adding, what to agree with the backend in week 1 even if you build it later, and how to implement it without PII leaks.

When to consider it

Add inbound sync only when all of these are true:

If your demo story works from the external feed only, skip inbound sync and say so in the README.

What to agree in week 1 anyway

You do not have to build inbound in week 1. You should still agree the usage contract with the backend: which app tables you may read later, which columns stay off limits, and whether app_events will exist.

Ask for an append-only app_events table early if you might model behaviour later. It only records events after the table exists, so a table created in week four gives you one week of history.

How inbound sync fits the architecture

flowchart LR
    EXT["External<br/>data source"]
    subgraph data["Data Track"]
        RAWAPP[("raw_app<br/>copy of app tables")]
        PIPE["Ingestion<br/>and dbt marts"]
        DEDB[("Data platform<br/>Databricks catalog")]
    end
    subgraph backend["Backend Track"]
        API["REST API<br/>Spring Boot"]
        APPDB[("Application database<br/>app schema")]
        ANA[("analytics schema<br/>you write, they read")]
    end
    subgraph frontend["Frontend Track"]
        UI["Web application"]
    end

    EXT --> PIPE
    RAWAPP --> PIPE
    PIPE --> DEDB
    DEDB -->|"outbound sync"| ANA
    ANA --> API
    API <--> APPDB
    UI <--> API
    APPDB -->|"inbound sync"| RAWAPP

    classDef de fill:#e8f4ea,stroke:#4a8055
    classDef be fill:#e8eef7,stroke:#4a6080
    classDef fe fill:#f7f0e8,stroke:#806a4a
    class PIPE,DEDB,RAWAPP de
    class API,APPDB,ANA be
    class UI fe

The inbound sync (app tables copied into your raw_app layer) runs from the application database back into your Databricks catalog. It is the mirror of outbound publish: outbound sends marts to analytics; inbound copies selected app tables into raw_app for dbt.

The backend grants a read-only role for it:

-- Backend runs this once. This role can read the app, and change nothing.
CREATE ROLE analytics_reader LOGIN PASSWORD 'a-different-one';
GRANT USAGE ON SCHEMA app TO analytics_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO analytics_reader;

Two roles, two jobs. analytics_user writes marts and reads nothing else. analytics_reader reads application tables and writes nothing at all.

The copy lives in your warehouse

Land the application's rows in a schema called raw_app on your platform, and treat it as a dbt source like the external feed. Do not build analysis tables inside the application database.

Where Schema Direction Written by
Your data platform raw_app In: copies of application tables Your inbound sync
Application database analytics Out: your published marts Your outbound sync

Current state is not behaviour

Application tables tell you what a user's saved items are now. They do not tell you when they saved them unless the backend records events. Copying only state blocks questions like "how did usage change over the project."

Deletes disappear quietly. If a user removes a saved item, incremental loads may miss the delete unless the backend soft-deletes or you reload small tables in full. Write down which approach you use.

Copy no personal data. Select only the columns you need. Hash user ids rather than carrying email or name into the warehouse.

What a good inbound sync looks like

Inbound sync is simpler than outbound: nothing serves those rows directly to end users.

Wire it into the DAG after dbt build only when you have models that read raw_app. Many teams add it as a fourth task once outbound has been stable for a week.

Extra reading


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 https://hackyourfuture.net/

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.