Read ten Analytics Engineer job postings and you get the same list: SQL, dbt, a cloud warehouse, Git, an orchestrator, maybe Python. It's accurate, and the least interesting part of the job. Those tools are well documented and mostly learnable in a few focused weeks. Nobody's career stalls because they couldn't figure out dbt run.
What takes longer is the method — the order you do things in, and what you refuse to skip. It decides whether the numbers coming out the other end are worth acting on. What follows is the method I use, in order, against a real project: AE-01, an ELT pipeline that loads stock market data with dlt and models it in dbt on DuckDB, portable to Snowflake.
The handoff is the job
Data moves through a chain of roles, and every link is a handoff. The Data Engineer lands raw data in the warehouse. The Analytics Engineer turns those raw tables into a modeled, tested, documented layer. The Data Analyst queries that layer to answer questions. The Business Analyst turns answers into decisions.
Each of those arrows is a contract. The DE's contract with me is that the raw table lands on schedule with the columns it promised. My contract with the analyst is stricter: a fact table with a stated grain, keys that are tested rather than assumed, and a description for every column that was checked against the data. The analyst's contract with the business is that the number on the dashboard means what its label says.
Most portfolios show the artifact and skip the contract: a chart, or a notebook that ran once on a laptop. They never show what the next person downstream can rely on — which is unfortunate, because the contract is the job.
Underneath the Analytics Engineer's link in that chain sits Kimball dimensional modeling: facts for measurements, dimensions for context, grain declared before anything else. It predates every tool in the modern stack and still describes what a BI tool expects to find on the other side of the connection.
The method, in order
1. Start from the business question, not the dataset
The instinct with a new dataset is to open it and start poking. Resist it. Start instead with the decision someone will make differently because this model exists. For AE-01 the question is plain: can we track daily price behavior across a basket of tickers, consistently, with numbers we can defend? It decides the grain, the tests, and which columns earn a place in the fact table.
If you can't say what decision the model serves, you're decorating.
2. Write the grain down before the SQL
The grain is what exactly one row represents. One row per ticker per trading day. One sentence, written into the .yml before the first SELECT. This is the oldest discipline in Kimball and the highest-leverage minute in the project: the grain determines the keys, the joins, the tests, and whether a SUM in a dashboard six months from now quietly double counts.
Then prove it. A comment claiming "one row per ticker per day" is only an assertion — it can stop being true and nothing happens. In AE-01 the fact carries a surrogate key built from ticker + trade_date, and a unique test on daily_price_key is the proof that the grain holds. If the grain breaks, the build breaks.
3. Layer deliberately: staging → intermediate → marts
models/
├── staging/ # one model per source: rename, cast, no joins
│ ├── _sources.yml
│ ├── stg_market__prices.sql
│ └── stg_market__tickers.sql
├── intermediate/ # business logic: daily_return, trading calendar
│ ├── int_daily_prices_enriched.sql
│ └── int_trading_calendar.sql
└── marts/ # star schema: keys and joins, nothing clever
├── _marts.yml
├── fct_daily_prices.sql
├── dim_tickers.sql
└── dim_dates.sql
Staging renames and casts. No joins — a rule that looks arbitrary until you inherit someone else's project, and the convention every Analytics Engineer reads for. When staging is boring, you can trust it and move past it.
Intermediate holds the business logic: daily_return, calendar attributes, anything that took a judgment call. Marts stay thin — keys and joins. Business logic living in intermediate/ is precisely why the marts stay readable: when someone asks how daily_return is calculated, there is exactly one file to open.
4. Test what would break silently
Loud failures take care of themselves; a pipeline that errors out gets fixed the same morning. The expensive failures are quiet — a join that fans out and doubles every number, a null key that drops rows from a dashboard nobody reconciles. Those ship, get believed, and get found by a stakeholder in a meeting.
So point the tests there: not_null and unique on every key, relationships from the fact to each dimension so an orphan row is caught before the analyst finds it, and business rules as singular tests — the domain assertions no generic test knows to make. AE-01 lands on 22 tests. That was never a target; it's what the keys and relationships asked for.
Prioritize by risk. Coverage percentage is a vanity metric.
5. Document as you model
Write the description while you still remember why. A month from now you'll write "the daily return" and move on; today you know it's a close-to-close change on unadjusted prices, which is a meaningfully different statement about the world. Descriptions and lineage are cheapest, and most accurate, while the reasoning is fresh.
This is also the one place AI clearly earns its keep. An LLM drafts the first version of the column descriptions from the data profile, which removes the tedious part; a human then validates every line against the actual data before it's committed. The draft is a time-saver, never an authority. A language model will describe a column confidently and wrongly, and a wrong description is worse than a missing one: a missing description gets questioned, a wrong one gets believed.
6. Prove reproducibility
Clean room: empty warehouse, fresh environment, run the README's commands in the order they're written, and check the results against the numbers you published. Same numbers, or it isn't done. If a step only works because of something already on your machine, you have a demo, not a pipeline — and the clean-room run is the only honest way to find that out.
7. Ship evidence, not adjectives
"Robust, scalable, production-grade" tells a reader nothing, and anyone who has built something reads it as a warning. Ship numbers instead. AE-01 builds 7 models and 22 tests with 0 errors over 5,010 rows — 10 tickers, roughly two years of trading days — and the lineage graph in the README is a screenshot of the real one.
Then state the limitations out loud. AE-01's README says plainly that prices are unadjusted: splits and dividends are not back-propagated, so daily_return is a raw close-to-close change, not a total return. That sentence costs nothing to write and it's one of the most valuable lines on the page: a reader who finds a limitation on their own stops trusting everything else you said, while a reader told up front reads the rest as honest.
What survives a tool migration
Every tool in that pipeline is temporary. Warehouses get swapped for cost reasons, orchestrators turn over every few years, and dbt is not guaranteed the next decade. When that day comes, everything tool-specific goes in the bin: the Jinja, the YAML syntax, the profile config, the CLI flags.
What crosses over intact is the business question, the declared grain, the layering, tests aimed at what breaks silently, documentation checked against the data, and the contract with the person downstream. That list would have been just as correct on Postgres in 2005, and it will still be correct on whatever replaces the warehouse in 2035.
Tools are how you do the work this year. Method is what survives the migration — and it's the only part your next teammate actually inherits.