Orchestrating Dynamic Data Flows, AgileData Engineering Pattern #1 - Episode #67
30 Jun 2025 · 22 min
Shane Gibson and Nigel Vining discuss their approach to orchestrating data flows using manifests, pub/sub messaging, and context-driven patterns. They explain how this method manages data pipelines more efficiently and addresses key architectural challenges in data engineering.
Foreign.
Welcome to the Agile Data Podcast where we talk about the merging of agile product and data patterns in a simply magical way. Welcome to the Agile Data Podcast. I'm Shane Gibson.
And I'm Nigel Vining.
Hey Nigel. Today we're trying a new thing. We're going to start a series where we effectively describe one pattern in each episode that we use. And I'm thinking of it as pages in our pattern library. The first one we want to talk about is the way that we orchestrate the data flows in our way of working. So why don't you start off by giving me an overview of the patterns that we use to engineer that particular task.
So we do it a little bit different. After many years background using the likes of composer and airflow to deploy DAGs and and create run manifests, we thought we'd be a little bit different and use a context driven generate the manifest on the fly approach. So effectively what we do is we create our data rules for transforming loading data. We put them into our context database, and then what happens is the first piece of data that turns up effectively triggers a refresh of that data table. And then this is where it changes from the usual. So as soon as that table is loaded within pause and we do another lookup of the config and we basically say, what are all the downstream objects that hang off this table? Now that's loaded. And from that we build a manifest on the fly. So we effectively say, great, we need to load this table depends on this one, so we will load it first. So we build that manifest and then we seed off all those jobs. We basically set them off all at the same time. We attach a tag to each of them and then we wait for them to finish. They all run in parallel and as each one finishes, it checks that manifest and goes, am I the last? Nope. Am I the last? Nope. Until we effectively get the driving table, which would start the next flow. So once that table comes back and says, yep, loaded, we do another config lookup and we start again. We create another manifest and we keep repeating this until we've effectively got nothing left in the manifest. The beauty of this approach is in our early days we did a lot of rebuilds and changing context. Shane would introduce new context while jobs were already running. He'd delete tables or he recreate them with a different name. So the whole time what was happening is we were getting lots of failures until we went to this pattern of context and manifest on demand, where effectively the whole time it was always checking and saying, oh, new piece of config's turned up, got another table to load and it effectively slotted into the mix. And it was all quite seamless and painless for me. So that's our pattern, a nutshell.
And the reason I like it is because if we think about each of our blobs of transformation code has been a rule. That's the term we use. I can go and add a new rule. So I can say there's a table sitting in history. Think raw bronze, I need to do something nasty to the data to bring it through into our designed layer. And I can go and add a rule, I can add a small piece of logic that takes that data and moves it into our design layer. And as soon as I do that, the next time there's a scheduled execution or an execution that's triggered for a load, the manifest, the DAG directed graph for that flow will dynamically rebuild itself. And so it just self heals. And then as you said, when I break my own rules for naming conventions, I can go and rename things and again that the data flows self heal themselves. So effectively I can do the work I want to do. And I don't need to care about what the directed graph for those data flows are. I don't need to care about the dependencies, because the context engine, this pattern we use for orchestration of these data flows is doing it for me. Right. I just don't care. And importantly, it's doing it for you because you don't care either. Right? There's effectively we can make any change we want and the machine just takes care of it. So I think for me that was the biggest value if I think about the alternatives. So one of the core patterns we use, which you can talk about in another podcast, is this idea of context driven. So everything we do, every piece of logic is stored in a repository and everything that needs to execute is hydrated or generated dynamically from that repository. So what that means is when we make a change, we can dynamically regenerate those data flows versus the old way of doing it, which is we'd hold that context in one place and we'd have a physical instantiation of it as a set of dags. And now I have to sync it. Yeah, I have to, like, I want to change the rules. Okay, now I need to change the dag. And those things used to always get out of sync. We kind of like that's not the way it's meant to work. Oh shit. There's this piece of Code stored as a stored process somewhere as part of that dag. Well, the dags were a stored process. And that syncing problem used to be a real problem in the old patterns.
Yeah. And even today, I occasionally struggle with consistent DAG deployment. If there's multiple people, engineers, developers, making changes in that space, you can effectively get a little bit of drift of what should be running and what's associated with other things, because there's a little bit of change going on as commits are happening. Whereas this way we effectively leave it to runtime. And it's at runtime as an object loads, we look up what's next. So it's always the freshest piece of lineage or manifest that you can have for any object. It's always up to date because it's always doing a query back to our context layer to say, hey, what's next? And then it starts next, and that's cool until that's finished. So I like it for that because it takes care of itself and also.
All of the maps that we generate. So our data catalog, our lineage graphs, everything else is driven off that context versus the previous generation, where we would have just treated all the code and all the schemas and all that structure and all those schedules as exhaust. We would have sucked it into the catalog to try and render it. We flipped that model, which is you define the things in context and then everything else is generated off it. It's always up to date. Because I cannot do a piece of work without the context being entered. I cannot create a transformation code, I cannot create the idea of a table, I cannot create the idea of a schedule, I cannot orchestrate dependencies across tables without first creating that piece of. So again, that forces us to make sure that anything I'm looking at is the latest version, because there is no choice.
Yeah. Yep. And I guess the natural question that others would ask in the profession, what about when a change is made that breaks something? And this is an interesting one, because effectively, when any changes are made, we validate them and we don't allow updates to the context layer unless it's a valid update. So every change that's made won't update the latest production context until it's valid. So we effectively stop anything getting into the context layer that's going to cause a problem. It stays at a draft, effectively, until it's validated. So when the manifest is generating itself, it's always looking for the active record. And it's fine for this to be draft floating around. It won't touch it until it's valid and then it flips over to replace the current production regod.
And then there's a bunch of attributes for that context. So when I define a table or a piece of code, I can define that. It's what we call auto sync, that it will execute that manifest, that data flow. Whenever a row turns up in one of its dependencies, Salesforce sends us some records, it hits that history tile or table, so raw bronze. Then if it's set to auto sync, it will then trigger the hydration of that manifest and run it. But if I flip it and say that for that piece of code or that table, it's manual sync, custom sync, and I say it's going to only run at 7am on a Monday, then that's when it triggers the hydration of that manifest and running that code. So there's a bunch of attributes that are actually really important when you start moving all your data flow stuff back to a context layer.
Yeah, absolutely. And that's a good point. You touch on effectively allowing that orchestration to start based on a record turning up or a file turning up, or a schedule that's been attached to it that says run it daily at 7am, which is quite a common pattern for pipelines. But 99% of our pipelines tend to start when something arrives. File some data, which effectively triggers the pipeline to start and pull that data all the way through the layers.
And then the other pattern that we use a lot in there is this idea of pub sub, as you said, it's a fire and forget. It figures out all the steps. It fires them off into, effectively a queue. It waits for the queue to run. So again, as you always do with patterns, you're using other patterns to make this pattern really efficient and effective.
Yeah. So the driver, as I said, at the start of. Done. Are you done? Are we ready to run something? Those are all pub sub messages. So we have a standard pub sub message which basically says I'm done. And that's attached to everything in the execution layer. So anytime a table is refreshed, we get a I'm done message off the end of that. And then we basically look for those messages as we walk through our layers. So that's basically, I guess, our wrapper pattern which holds it all together. Has something happened? Yep, cool. Trigger something else. And we just use messages and then.
What'S our average runtime? It's always dependent on. We use bigQuery under the covers and Google Pub sub and it depends on how much data on that from what you're seeing with all the different customers we've got and the volumes of data, what's an actual execution for a standard data flow? Way to go from arriving data in history all the way to data being consumable in that final layer.
It's generally quite fast. We have very little latency because there's no overhead. And like an airflow example, there's no overhead of starting up, I guess execution pods, kubernetes, pods to run a task, to shut down, to start up and run another task. Because we effectively just using a message, we can tell BigQuery to run something very quickly and in parallel as much as possible. So our execution times are generally very fast because there's zero overhead. We're Basically just telling BigQuery, here's a list of things I want you to do and letting it do it.
And our costs are low because we're not running a container, Kubernetes and EC2 anything like that permanently to run the orchestration or the scheduling engine. Because we're using PUB sub and cloud functions but moving to cloud run, it's doing all that magic for us. So it turns itself on, the code's deployed, it's run, it's then destroyed and we only get charged for that period of time that it's actually running. Rather than have a 247 service sitting there to just schedule and orchestrate our code.
Yeah, that's exactly right. So we have no concept of an orchestration server running in the background. We are Effectively creating the BigQuery jobs on the fly, letting it run them and waiting for that I finished message to come at the end of it. So there is never a service that is physically waiting for a job to run and costing us money. Basically BigQuery is running jobs and when it finishes we get notified and we automatically start the next job. So it makes it very economic to run. And we can run hundreds of jobs, thousands of messages, but it basically costs us nothing because there's nothing running to orchestrate that. Those are just messages bouncing around, starting jobs, waiting for jobs to finish. Very effective.
But there is an anti patent for this because we only use this for what we call micro batching. We think about anything above 15 minutes of refresh latency, this works. For anything below that, we start hitting a problem and it's not a problem of pattern around this orchestration of data flows. It's a problem problem of the dependencies of everything else we've built. So if we think that we have a layered data architecture, history, design consume, we Use tables under the COVID in BigQuery. So there's a dependency on who's writing to them. And we know we can orchestrate this stuff very quickly, but if we move to a streaming model, if we wanted to pick up one row and push it all the way through this, we would actually end up slightly rearchitecting the way we deploy the code to pub sub. We wouldn't actually fire off 10 tasks and wait for them to daisy chain and orchestrate themselves and have dependencies. What we'd probably do is look at the entire piece of code for the end to end orchestration and then submit that to run as a single code stream in BigQuery end to end as one task, wouldn't we? Because the pattern we've got naturally breaks the task down into nodes and links. It says run these and when they're done run these and. And then if we keep firing it with volumes of streaming data, we will actually end up having a bottleneck, won't we? So that's an anti pattern. We would use the same context approach. We would still hydrate and fire, but we would slightly change the way the pattern is deployed at runtime to handle that streaming volume.
So in that case we would let data stream constantly into the landing area, which is what we do in some projects. And then effectively, I guess our compromise around that one is then we would micro batch at 15 minute intervals from that stream data and move it through in micro batches currently. Otherwise yes, we would architect to stream through the layers and we would probably run a two speed pipeline. In that case we would micro batch it, say maybe 15 minute intervals. But then what we would also do is we would stream around the outside data directly to the consumer layer for the required attributes that need to be consumed in real time. And then we would catch up the rest of the layers of our slower data by bringing it through the normal pipeline. And then we get the best of.
Both worlds, effectively adopting a lambda architecture. Right. But the majority of our patterns would survive for this orchestration of data flows. But we know that we actually have to change or tailor or augment some of them when we hit that anti pattern for the way that we're currently running it and we're currently designing it. So apart from a little bit of tweaking for streaming, is there any reason you wouldn't use this pattern? Any use cases where you think this dynamic hydration of the data flows and this creating a manifest and this pushing it off to fire and forget and run, Is there any reason that you wouldn't use this pattern?
No. We've seen it run day in, day out for coming up five years now. The original architecture and code that was deployed back in the first year is largely exactly the same. We have not changed the core pattern. So it's run happily across multiple customer use cases for five years. I think that's a fairly good testament to it's a pattern that works.
I think the other thing we can do is we can, if we needed to, we can scale horizontally or scale vertically. If we ever do get to a situation where we need to reduce the latency time of the end to end process, we can scale up the bigquery instances and make them execute faster. We could chunk the data flows down into more domain orientated buckets and then figure out how to paralyze the runs of them. So again, we've still got lots of headroom of how we can iterate this pattern if we ever strike a cost or a performance or a complexity issue. So we know we're always going to be iterating it. But like you said, it's been fairly good and bulletproof. I think the other thing is you've spent quite a lot of time on what we call the blast radius. As you started building out that core pattern as code and as I started doing the things that I would expect to do as a non data engineered data, I should be able to do this. And you suck your teeth going oh my God, you've built those safety nets around that pattern. So if you talk about the fact that I can create a piece of context, but it's not actually treated as part of that production data flow, it's not executable when it runs until it meets all these checks. We're effectively bringing in the peer review process, the git review, the PR pull request, the data quality tests, all of that's happening before any of those things allow the data flow to actually execute as a new version.
Yeah. And so the other thing that's wrapping this is in the context layer we've got a couple of flags that are applied to the context. So if something has changed in the context layer and successfully deployed into production state, we effectively put a flag on it that says this config has changed. It's going to require changes of downstream objects to flow this change. For example, you've added a new column into a table and now you expect that column to flow through and hydrate and all the layers downstream of it. So effectively we tag that object and say, hey, this has changed. You're going to need to recreate it and you're going to need to check all the things downstream of it to make sure that they're going to be okay. So when we come to run that manifest, we run it as usual. But effectively we hid an object and say, oh, there's a flag on this that says it's changed. So we identify the change, we usually recreate the table and we roll the watermark back, we reload that table. That's cool. We get to the next one and we say, great, something upstream of me has changed. I need to make sure how that's going to work. So what we actually do is we do another validation of that context to make sure it's 100% going to work with that change. And at that point, if we determine, oops, this is going to have to be rebuilt as well, we do that and we keep doing this all the way, the end of that manifest. But we also stop the manifest if for some reason we've actually caused something that's not going to keep working. And we stop and we raise an alert and say, hey, this context is now going to be invalidated because something two or three layers upstream has changed enough that I can't fix it for you automatically. You're going to have to look at change and then basically say what you need to do. So that was quite a breakthrough because we used to spend a lot of time manually trying to fix pipelines that stopped for small things like karma being added or removed. Whereas 99% of the time we can handle that automatically just by doing a full rebuild and refresh of the table and then revalidating the rules. And generally everything takes care of itself because we wanted it to be self healing, because there's nothing worse than pipelines that fall over just because you've added a column. Whereas something like that's easy. A column can just be added, populated, and away you go. So that was quite a breakthrough. Basically putting those extra lags in the context.
Yeah. And effectively they're like stage gates. It's basically every time it's validating. Am I still around the next step? And then in the beginning, like you said, we had lots of times where it stopped. I would do something and then it would say, oh, actually you're not meant to be able to do that. But as each one of those happened, we'd go in and we'd either fix the way I created that context so I could no longer create it in a invalid way, or we'd automate the rebuilding the deploy and destroy model that it was trying to rebuild so it could rebuild itself safely. So again, we didn't change the core orchestration pattern, we changed all the other things around it to make sure that pattern was bulletproof and ran. And we didn't get call out at 2 o' clock in the morning because a data flow had stopped or at 7 o' clock to have a look at it and have a whole lot of catch up work to rebuild it. So I think those supporting patterns were as important as this core orchestration of the data flows by them being dynamic and using Manifest Publisher.
Yeah, that's exactly right. It makes it all quite robust and as I said, generally the pipelines run very accurately without a lot of tension.
Excellent. All right, well I think that one's done so orchestrating data flows in a dynamic way using Manifests and Pub Sub. Right. Another one next week, but for now, hope everybody has a simply magical day. And that Data Magicians was another Agile Data podcast episode. If you want to learn new patterns that you can apply to craft your own agile data way of working, head over to agiledata IO.
More from Shagility


Listen now | Recording of AURA - Tim Frazer & Shane Gibson on 2nd September 2026








