Period close in a live operational system is one of those operations that feels straightforward until it isn’t.
You trigger it. It runs. Somewhere in the middle — network blip, timeout, server restart — it fails. Now you don’t know what committed and what didn’t. Do you rerun it? If you do, do you double-count everything that already went through? If you don’t, do you have gaps in your data?
This is the exact problem we faced building the stock control pipeline for New Zealand’s largest OutSystems platform, serving companies including McDonald’s NZ across 10,000+ daily users. Period close was triggered daily across a multi-restaurant estate, calculating theoretical vs actual ingredient consumption from product sales data. Getting it wrong — even once — meant corrupt variance data flowing downstream to operations and accounting teams.
The solution was to design the pipeline to be idempotent from day one.
What idempotency actually means in this context
Idempotency means you can run an operation multiple times and always get the same result. The second run, the third run, the tenth run — all produce identical output to the first.
For a period-close pipeline, that means: if you trigger it twice for the same period, the data looks exactly as if you’d only triggered it once. No double-counting. No orphaned records. No silent corruption.
This sounds obvious. It almost never gets designed in from the start.
How we built it in OutSystems O11
The core pattern is CreateOrUpdate everywhere — never bare Inserts.
In OutSystems, the default instinct is to Insert new consumption records when period close runs. That works fine on the happy path. It breaks the moment you need to rerun. Instead, every write in the pipeline uses CreateOrUpdate logic: look up the record by its natural key (restaurant + period + ingredient), update it if it exists, create it if it doesn’t. The result is always the same regardless of how many times you run it.
The natural key is important. You need a stable identifier that means “this consumption record for this ingredient in this period at this restaurant” — not a surrogate database ID. Surrogate IDs are generated on Insert, so they break idempotency immediately. Natural keys are deterministic: same inputs, same key, every time.
Beyond CreateOrUpdate, the pipeline also handles orphan cleanup. If a period close reruns after a partial failure, you may have records from the first run that shouldn’t exist — partially-written states, superseded calculations. The pipeline checks for and cleans these before writing new data, not after. Cleanup before write means the final state is always consistent.
The failure modes this prevents
Without idempotency, these are the failure modes you’re managing:
Double-counting on rerun. The most common. Consumption records get inserted twice because the pipeline can’t distinguish “I already processed this” from “this is new.” Variance figures inflate. Operations teams see phantom shortfalls. Trust in the system erodes fast.
Partial period state. Pipeline fails halfway. Some restaurants have close data, some don’t. The period is neither open nor closed. You can’t safely continue forward and you can’t safely roll back. You’re stuck.
Silent corruption. Worst case. Pipeline appears to complete. Some edge case — an empty period, a restaurant with no sales data, a timeout on a specific tenant — causes a write to skip. No error. No alert. The gap sits in your data until someone notices a reporting anomaly weeks later.
Idempotency doesn’t eliminate failures. It means failures are recoverable. Rerun it, get clean data, move on.
Multi-tenant complexity
The pipeline ran across a multi-restaurant estate, which added a layer. Each restaurant is a tenant. Each tenant has its own stock periods, its own ingredient catalogue, its own sales data feed.
The natural key for each consumption record therefore includes the tenant identifier — restaurant ID is part of what makes a record unique. This means the idempotency guarantee holds per-tenant: reruns for restaurant A don’t touch restaurant B’s data, and a failure on restaurant C doesn’t corrupt the records already written for restaurants A and B.
In OutSystems, this means your query architecture needs to scope every read and write by tenant from the start. Not as an afterthought. If you bolt tenant scoping on later, you’ll find places where it wasn’t applied consistently and your idempotency guarantee has holes.
When to build this in
The right time to design for idempotency is before you write the first action.
Once you have bare Inserts in production, retrofitting is painful. You need to audit every write path, introduce natural keys where there weren’t any, rewrite Insert logic throughout, and verify that your new behaviour matches the old happy path exactly. It’s doable, but it’s a week of work that could have been an hour of design upfront.
The signal that you need it: any operation that modifies significant state, runs on a schedule or trigger, and would cause data problems if it ran twice. Period close fits all three. So do invoice generation, payroll runs, batch reconciliations, and most scheduled export pipelines.
If the answer to “what happens if this runs twice?” isn’t “nothing different,” you need idempotency.
OutSystems-specific notes
A few things that are specific to O11:
- CreateOrUpdate is your friend. OutSystems generates this for every Entity by default. Use it. The
UpdateOrInsertaction in the Data layer is the same thing. Pick one pattern and be consistent. - Avoid auto-number IDs as natural keys. They’re generated at insert time and are meaningless for deduplication. Composite natural keys from your business domain are the right tool.
- Transactions matter. OutSystems wraps server actions in transactions by default. Understand where your transaction boundaries are and make sure cleanup + write happen in the same transaction where possible.
- Timers and BPT. If you’re triggering period close via a Timer or BPT process, those have their own retry logic. Know whether your platform will retry on failure — if it will, your pipeline needs to be idempotent regardless of whether you think reruns are likely.
The broader principle
Idempotency is an architectural property, not a feature. You don’t build it because a stakeholder asked for it. You build it because operational software running at scale, on a schedule, across multiple tenants, in a live environment — needs to be safe to operate.
Failures happen. Networks blip. Servers restart. The question is whether your system recovers cleanly or requires manual intervention every time something goes wrong.
On New Zealand’s largest OutSystems platform, we designed for the failure case from day one. Period close runs cleanly. When it doesn’t, a rerun fixes it. Nobody has to touch the database.
That’s the standard.