Larkfield hands you a connection string and walks away
Larkfield ships a product that issues rewards on a public ledger, and somebody has decided the data team should be able to report on it. How the ledger is built, agreed and kept tamper evident is settled and is not tonight's subject; that belongs to learn-blockchain-with-phoebe, which is the prerequisite here. Tonight starts at the point an engineer usually starts, with a source that nobody has profiled yet, and the first honest hour spent working out what is actually in it.
★ What you walk out with tonight. A correct answer to the three questions you ask of any new source, for this one: the row, the columns, the grain. The reason the natural key looks exactly like a customer id and behaves exactly unlike one. A clear-eyed comparison against a normal ingestion, with the losses and the gains kept apart rather than blended. And a measured look at the raw form on Larkfield's month, which will make the case for session 2 better than any argument could.
LiveThe three questions, before anything else4 min▶
Every engineer has the same opening move on an unfamiliar source and it is not reading the documentation. It is: show me one row, tell me what the columns mean, and tell me what one row is one of. Grain first, because every aggregation you will ever write is wrong if the grain is wrong, and a wrong grain does not raise an error. It quietly doubles a count.
The move works here too, which is the useful news. What changes is the answers. One row is a transaction. The columns are thin and one of them is opaque until you decode it. The grain has two defensible readings rather than one, and which of them you pick determines whether a whole category of analysis is available to you later. None of that requires new engineering instincts. It requires the ordinary instincts applied to a source that will punish skipping the step.
Larkfield's first analyst skipped the grain question because the source looked simple. They wrote a count over a table that had been exploded to one row per transaction input, reported it as transactions, and were about eleven percent high. Nothing errored, nothing looked odd, and the number was wrong for two months. That is the specific failure the grain question exists to prevent, and it is not a chain problem. It is the oldest problem there is, arriving through a new door.
Self-studyThe words that keep coming back4 min▶
- Address. The only identifier a row carries. Persistent, and not a person. Part 1 tonight, taken apart in session 3.
- Entity. The real party behind one or more addresses. Never present in the data.
- Transaction. One record. It names input addresses, output addresses and a payload, and it sits at a known position in a known block.
- Payload. The part of the record that is meaningless until something decodes it against the definition the deploying code used. Session 2.
- Grain. What one row is one of. Here there are two answers, one per transaction and one per transaction input, and both are used.
- Co-spend. A single transaction spending from two addresses at once. Visible only at the input grain. Session 3.
- Retraction. The upstream withdrawing history you already loaded and reported. Sessions 6 and 7.
- Idempotency, late-arriving data, append-only loads. General pipeline craft, and it all applies. It lives in learn-data-engineering-with-phoebe and is not repeated here.
Self-studyWhere the ten sessions go3 min▶
Two sessions on getting the data into a usable shape, three on counting things that resist being counted, two on the upstream taking history back, and three on what all of that does to a model, a bench and a review.
- 1 and 2. The dataset as it arrives, then the pipeline that turns it into tables anybody can query.
- 3, 4 and 5. The co-spend heuristic and union-find, how to measure what it got right, and what happens when some of the parties are being paid to be hard to count.
- 6 and 7. A chain reorganisation landing in a warehouse that already published, and the two separate defences against it.
- 8, 9 and 10. Training on data where your features and your model are both visible to the people generating the rows, a bench that fooled its own author, and the whole Larkfield estate driven live.
One row, thin columns, and a grain with two answers
14 min liveStart with the record itself, because everything downstream is a consequence of its shape. A transaction names some input addresses, some output addresses and a payload, and it sits at a position in a block. That is close to all of it. The interesting part is not what is there; it is that the one relationship the record does state is the one relationship everybody ignores, and the column that looks most like a key is the one that is not.
LiveWhat one row is, and the relationship it does state4 min▶
The record is thinner than most sources you will have ingested. There is no event name you did not have to derive, no user column, no timestamp you can trust to mean what a product timestamp means, and no free-text field to profile. In exchange there is total completeness: nothing was sampled, nothing was dropped by a collector, and no agent failed to fire. That combination is rare and it changes where the risk sits. The risk is not missing rows. It is misreading the rows you have.
The one relationship a record does state is the one worth noticing on day one. When a transaction draws on two input addresses, whoever built that transaction had to be able to authorise both. That is a fact about control, stated by the structure of the record rather than inferred from behaviour, and it is the only such fact in the whole dataset. Most teams skate past it because it looks like plumbing. It is the entire basis of every entity count anyone will ever ask you for.
Everything else you might want about a party - who they are, where they are, what they bought, whether they came back - is not thin, it is absent. There is no column with nulls in it. There is no column.
LiveThe grain question has two correct answers4 min▶
One row per transaction is the natural grain and it is what most people mean when they say a chain is a list of transactions. It is the right grain for counting activity, for time series, for anything where the unit of interest is an event. It has one property worth writing on a wall: the input addresses are a collection inside a single row, so the co-spend relationship exists in your table but is not joinable.
One row per transaction input is the exploded grain. The same transaction id now appears on several rows, one per input address, and suddenly the co-spend is a self join rather than an array operation. That is the shape session 3 needs. It is also the shape that produces a wrong transaction count the first time somebody writes count(*) against it, because the row count is now the input count.
Keep both. Neither is a compromise and neither replaces the other. The failure is not choosing wrongly, it is choosing silently, so that six months later nobody in the room can say which grain the table downstream of them is at. Write it into the table name if you have to.
LiveThe key that behaves like a key and is not one3 min▶
An address is the most key-like thing in the dataset. It is stable over time, it is unique as a string, it never arrives null, it joins cleanly, and every profiling tool you point at it will report a well behaved high-cardinality identifier. A uniqueness test on it passes. A referential integrity test on it passes. Nothing you can automate will tell you anything is wrong.
What is wrong is the cardinality of the thing behind it. On Larkfield's generated month there are about 1,000 addresses and 304 real parties, and no column, table, vendor or request will move you from the first number to the second. That is a 3.2 times error sitting inside a column that passes every check you own, which is a genuinely unusual failure mode and the reason it survives so long in real reporting.
Sessions 3, 4 and 5 are the response to this. Tonight the only obligation is to stop reading the column as an identity and start reading it as what it is: a handle on a spending capability, one party may hold many.
Self-studyProfiling a source with no owner to correct you4 min▶
Standard profiling still works and you should still do it: cardinality per column, null rates, value distributions, the widest and narrowest records, the tails. Point ydata-profiling or whatever you normally reach for at a month of it and read the report the way you always would. The craft does not change.
What changes is the follow-up. Normally a surprise in a profile becomes a question to the owning team, and the answer is either "yes that is a bug" or "no, here is the business reason". Here the surprise is the answer. If the widest transaction has two inputs and you expected more, that is simply what the source does, and there is nobody to confirm it and nobody to change it. Your profile is not a draft to be validated. It is the specification, and you wrote it by looking.
The practical consequence is to profile more often and to keep the profiles. A distributional shift here is not a data quality incident that somebody will explain to you; it is the only warning you will get that the thing producing the rows has changed behaviour.
A schema you do not own, and nobody to raise a ticket with
13 min liveNow the part that feels strangest to an engineer who has spent a career negotiating with upstreams. There is no upstream to negotiate with. Not an unresponsive one, not a slow one, not one behind three layers of ticketing. There is nobody. Four things you normally rely on are missing, and it is worth being precise about which of the four is a loss, because two of them are not.
...
LiveNo SLA, and no gatekeeper either4 min▶
Take the four absences one at a time and price them honestly, because a session that only lists the costs will be dismissed by anyone who has actually enjoyed working on this data, and they are not wrong to enjoy it.
No SLA is a real loss. Nothing promises you that rows arrive within a window, that the shape stays constant, or that anything at all continues. Your alerting cannot be a breach notification because there is no promise to breach. It has to be a distributional check you wrote against a baseline you measured.
No contract negotiation is a real loss and a small saving. You never spend six weeks agreeing a payload with another team. You also never get to ask for a field, and the field you need is frequently not there.
No backfill request is a gain wearing the clothes of a loss. You cannot ask anyone for history, and you do not need to. The whole history is sitting there and you can re-read it whenever you like, as many times as you like, without anybody's sprint capacity. Re-deriving five years of a metric after finding a bug is a week of compute rather than a quarter of politics. Almost nothing in a normal warehouse works this way.
No gatekeeper is the biggest gain and the one that gets understated. There is no access request, no approval, no scoped credential, no quarterly review, and no possibility of your access being narrowed because a team reorganised. The dependency that most often kills an analysis is the one that does not exist here.
LiveThe schema is code somebody else deployed3 min▶
The payload in a transaction is bytes. What those bytes mean is decided by the definition that the deploying code published, and different deployments publish different definitions for what a business would call the same event. You do not get a schema registry handed to you; you get a decision to make about how to interpret each family of records, and that decision is yours, which means it is also your bug when it is wrong.
Then it moves. A new version is deployed with a different definition, and your decode keeps running against the old one. Nothing fails. The rows still parse, or they stop matching and silently fall out of your filter, and either way the first symptom is a metric bending in a direction nobody ordered. There was no deprecation notice because there is no relationship in which a notice could be sent.
This is the single most important operational fact in the whole track and it is why the next session is about the indexer rather than about the chain. The decode is not a technical detail sitting under the pipeline. The decode is the pipeline's opinion about what happened, and it is where every disagreement between two teams' numbers turns out to live.
LiveWhy the raw form is close to useless3 min▶
Look back at the live box. The counts are correct, they were computed rather than asserted, and almost none of them answers a question anybody would ask. You can say how many transactions happened and how many addresses appear. You cannot say how many people, which activity was a reward and which was something else, whether anybody came back, or what any of it means in the product's own vocabulary.
That is not a criticism of the source and it is not a gap to be complained about. It is the normal condition of raw data everywhere. The difference is the size of the distance and who is expected to close it. On a normal source the producing team has already done the first two thirds of that work before you see a row. Here nobody has done any of it, and the whole of it is yours.
Which is the honest case for session 2, so it may as well be said plainly: the thing you are going to build is not a connector. It is an interpretation layer, it will be most of the work, and the numbers it produces will be your numbers rather than the chain's.
Self-studyWhere serious people disagree5 min▶
One camp says land the raw records first and keep them forever. Their argument is that your decode is an interpretation and interpretations are wrong, so the only reproducible artefact is the undecoded record, byte for byte, exactly as it was read. Keep that and you can re-derive any table from any point with a corrected decoder, and prove to an auditor that the derivation, not the data, was what changed. Throw it away and every historical figure is hostage to a decoder version you no longer have.
The other camp says that is a comforting story that does not survive contact with a real restatement. The raw bytes are only meaningful against the definitions in force at the time they were written, so preserving them without also preserving a complete, versioned map of those definitions preserves nothing but the illusion of reproducibility. And the cost is not theoretical: raw records are large, they are queried by nobody, and the reprocessing run that was supposed to justify them is quietly never scheduled. Better, on this view, to store the decoded rows plus the decoder version that produced them, and to treat a decode change as a schema migration with a proper backfill.
Both camps have shipped this. They are not really arguing about storage, they are arguing about which artefact carries the meaning: the bytes or the interpretation. Notice that the second camp's position depends on actually versioning the decoders, which most teams claim to do and few do. Ask the person in front of you which of the two they hold, and then ask them where their decoder versions are stored. The gap between those two answers is the real risk.
Answer the three questions on a source you own
15 min hands onNo code tonight. Six steps on a pipeline your own team runs, said out loud in the room. The aim is to find out how much of your current practice is standing on an upstream owner, because that is the part that will not survive the move.
State the grain of a table you own
Pick a table your team publishes. Say out loud what one row is one of, without looking at the code. If two people in the room give different answers, you have found tonight's most useful result and it has nothing to do with chains.
Find your exploded twin
Every warehouse has a pair of tables at two grains where one is the other exploded. Name yours. Then say how a new joiner would know which is which, and whether the answer is anything other than asking someone.
Name your upstream owner out loud
Who do you message when a row is wrong? Write the name of an actual person. Then delete the name and say what your last three data incidents would have looked like without them.
Count your ticket-shaped dependencies
How many things in your last month needed somebody else to agree: a backfill, a new field, wider access, a schema explanation. Mark which of those would be unnecessary on a source that is fully readable by everyone, and which would simply be impossible.
Look for a key that is not one entity
Device id, cookie, account, email, phone, session. Find one your reporting treats as a person. Say what its real cardinality per person is, and whether anything in your test suite would notice if it doubled.
Write the profile you would keep
For the Larkfield month in the live box, write down the three quantities you would baseline and alert on if nobody was ever going to tell you the source had changed. Bring them to session 2 and check them against the pipeline you would build.
The habit to keep. When a source has no owner, your profile stops being a draft and becomes the specification. So keep it, date it, and diff it. The question is never "is this value surprising", because there is nobody to be surprised at. The question is "is this value different from the one I measured last month, and did anything in my code change to explain it".
Three questions
An engineer builds one table at the transaction-input grain and reports a monthly transaction count from it. What happens?
The exploded grain has one row per input, so any transaction that spends from more than one address is counted more than once. There is no error, no null and no failed test. This is the ordinary grain mistake, and it is worth rehearsing precisely because the source looks too simple for it to happen.
A team runs a full profiling suite over the address column. Uniqueness passes, nulls are zero, cardinality is high and stable. What has the suite established?
Every mechanical property of a key holds. The property that fails is the one no profiler measures: values per real party. On Larkfield's month that is about 1,000 addresses against 304 parties, a 3.2 times error living inside a column that passes every test you own.
Which of the four missing pieces of a normal ingestion is best described as a gain rather than a loss?
No SLA and no contract are straightforward losses. The absent gatekeeper and the absent backfill queue are gains, and they come from exactly the same missing upstream owner as the losses do. An unowned schema drifts more easily than an owned one, not less, because nothing is obliged to announce the change.
Homework
Twenty minutes, on a pipeline you run and on the profile you wrote in step 6.
Trace one field back to its decision
Pick one column in a table you publish and find who decided what it means. Not who populates it, who decided. Write down how you would answer the same question if that person did not exist and never had.
Bring three baselines and one decode question
Your three quantities from step 6, and one thing in the Larkfield rows you would have to interpret rather than read. Session 2 builds the interpretation layer and puts a number on how much two teams can disagree while both being right.