Nonprofit & PaymentsField engagement

Many campaigns over one reliability core, because forking the gift flow per campaign is how reliability quietly diverges.

Architecture and delivery lead.
The approach, in four words
one core, many campaignsattribute at capturecampaigns are configurationrollups are projections
01Campaign goal and page
02Attribution at capture
03Per-campaign rollups
↓
One reliable gift core
webhook confirmed, idempotent, reconciled
Campaigns are rows over a single gift core rather than copies of the gift flow. Forking per campaign is how a reliability fix lands in one place and silently misses the others.
What was at stake

The organisation wanted to run many fundraising campaigns side by side, each with its own goal, its own page and its own reporting. The danger in that is slow and quiet. Each new campaign starts life as a copy of the last one that worked. Each copy drifts a little. A fix to the payment handling lands in this season's appeal and not in an older one that is still live. Eventually the same gift is handled differently depending on which page the donor came through, and nobody can say which pages are safe.

How this works

A campaign total should be a question asked of the gifts, not a number kept beside them.

A fundraising campaign is an appeal with a goal, a page, and usually a progress bar showing how close it is. Underneath, the gifts are the facts and the campaign is a label attached to some of them. There are two ways to keep a campaign's running total. The first is a counter: every time a gift arrives, add its amount to a number stored against the campaign. The second is a ledger: record every gift as its own entry, and compute the total by adding up the entries whenever someone asks.

The counter looks simpler, and it breaks in ways that cannot be repaired. Payment messages are delivered at least once, so a counter that adds on every message adds twice whenever a message is repeated. Refunds and disputes have to subtract, and a missed one leaves the total permanently high. Worst of all, when the counter disagrees with the money that actually arrived, there is no way to find out why, because a counter keeps only its latest value and none of the history that produced it. A ledger has none of these problems. Each entry has an identity, so a repeat is recognisable. A refund is just another entry. The total can be recomputed at any time, and any figure can be explained by listing the gifts that make it up.

Attribution is how a gift gets its campaign label. A donor arrives from an email, a social post or a printed appeal, and the link they followed usually carries a marker saying which campaign sent them. That knowledge exists in the moment, on the page the donor is looking at. Most payment processors let the merchant attach its own reference to a payment when it is created, and that reference comes back with the confirmation. Captured there, the campaign travels with the gift. Left until later, it has to be inferred by matching analytics sessions to payments, and that inference fails for every donor who blocks tracking, opens a second tab, or comes back days later to give.

A counter per campaign
Each message adds to total
a repeated delivery adds again
Refunds must subtract
a missed refund never comes off
Total disagrees with money
no history to say which is right
Totals computed from a ledger
Each gift recorded once
a repeat is recognised by its identity
A refund is an entry
the total corrects itself
Every total lists its gifts
any figure can be explained
The same gifts, counted two ways
A number that cannot explain itself cannot be corrected either.
FigureTwo ways to keep a campaign's running total. A counter remembers only its latest number, so any error becomes permanent and cannot be explained. A ledger keeps every gift, so the total can always be recomputed and traced back to the gifts that make it up.
The constraint

A campaign is a goal, a page and a way of knowing which gifts belong to it. That is a data model and an attribution question. The payment reliability underneath does not vary from one campaign to the next, so it should exist once, with every campaign as configuration over it. Treat a campaign as a variant of the gift flow and the reliability work has to be repeated in as many places as there are campaigns, which in practice means it is complete in none of them.

Where it usually breaks

Multi-campaign systems fail by copying, and the damage shows up as totals that do not add up.

The first campaign is built carefully. The second is a copy with a new goal and new artwork. A few campaigns later there are several gift flows that started identical and now each differ slightly. One has the fix for repeated deliveries, one has a newer form, one still confirms gifts on the thank-you page because it was copied before that changed. None of this is visible from outside, because every campaign page looks as though it works.

The second pattern is in the reporting. Each campaign grows its own total, kept in its own place and updated in its own way. Someone in finance adds up the campaign totals and gets a different figure from the payment processor's payouts. Nobody can explain the difference, because the totals were never derived from the same record, and the discussion turns into an argument about which spreadsheet to believe.

01

Fix drift

A reliability fix lands in the campaign that exposed the bug. The other copies keep the bug, and nobody keeps a list of which copy has what.

02

Totals that do not reconcile

Each campaign keeps its own counter. The sum of the campaigns disagrees with the money that arrived, and there is no history to show why.

03

Attribution by inference

The campaign a gift belongs to is guessed afterwards from traffic data. Returning donors, blocked trackers and forwarded links end up credited to the wrong campaign or to none.

04

The campaign nobody remembers

An old campaign page stays live long after anyone owns it. It runs an unmaintained copy of the gift flow, and it still takes money.

05

A progress bar with its own arithmetic

The public progress figure is computed separately from the reporting. The organisation ends up showing donors one number and its board another.

First campaign built carefully
one gift flow, well tested
Second copied from the first
new goal, new artwork, same flow
Bug fixed in the second
the first never receives the fix
Third copied from the first
inherits the unfixed version
Totals stop reconciling
and no one can say which copy is wrong
Copying a working page feels like reuse. It is the opposite.
FigureHow copied campaigns drift apart. Each step is reasonable on its own, and the damage only appears as the copies pile up. By the time the totals disagree, nobody can say which flows carry which fixes.
The fork

The reflex, and the fix.

Road not taken

Fork the donation flow per campaign

Pull

Each campaign gets exactly the flow it wants, and nobody has to negotiate a shared abstraction.

Why not

Reliability diverges across the copies. A fix applied to one campaign's flow never reaches the others, and after a while nobody knows which copies have it.

Road taken

One gift core, campaigns as data

Accepted

A campaign model and an attribution decision made up front, which is slower on the first day than copying a page that already works.

Bought

One place where gift reliability lives, and campaigns that are records with a goal, a page and an attribution tag.

Decision

The campaign is the part that varies. Whether the money arrives is the part that must not. So the varying part became data and the invariant part stayed single.

How it was built
01Campaigns as data
›
02Gift core
›
03Payment intent
›
04Idempotent webhook
›
05Per-campaign rollup
01

Campaigns as a data model

A campaign's goal, page and attribution are stored as records with no code path of their own. Launching a campaign is configuration, and the new campaign inherits every reliability fix made for the ones before it.

02

Attribution captured at the gift

The campaign a gift belongs to is tagged at the moment the gift is captured. Working it out later from traffic data produces a guess formatted as a report.

03

One shared idempotent gift core

Webhook handling, idempotency and reconciliation are written once and used by every campaign. A fix made there reaches every campaign the moment it ships.

04

Per-campaign rollups from one ledger

Each campaign's total is a view computed over the shared record of gifts. There is no separate counter per campaign, so the campaign totals agree with the ledger by construction.

Inside the key decisions

What each choice cost, and what it bought.

AA campaign is a record, not a code path

Every campaign is described by data: its goal, its page, its attribution tag. That meant designing a campaign model before the first campaign shipped, and it meant saying no to campaign-specific tweaks inside the gift flow. A request that genuinely needs the flow to change becomes a change to the core for everyone, which is slower to approve. What it bought is that a new campaign inherits every fix ever made, automatically. Forking would have been faster for each campaign and worse for the organisation, because each fork is a separate thing that has to be kept correct.

BAttribution captured at the moment of the gift

The campaign a gift belongs to is attached to the gift when it is created. The cost is that every way into the donation flow has to carry its campaign context, and a link that arrives without one produces an unattributed gift. The benefit is that attribution becomes a recorded fact about each gift. The alternative, matching gifts to campaigns afterwards from traffic data, produces a report that looks precise and is built on guesses about which session belonged to which payment.

CTotals as projections over one ledger

Campaign totals are computed from the shared record of gifts, never stored as separate running numbers. Computing a total takes more work than reading one, and displays that need a figure quickly have to be built with that in mind. In return the campaign totals cannot disagree with the ledger, because they are the ledger viewed through a filter. Separate counters per campaign would each drift in their own way, and the organisation would lose the ability to explain its own numbers.

DAn honest default bucket

A gift with no campaign context is reported as unattributed. That leaves a visible line in the reports that some readers will find untidy. It also means no report ever claims more knowledge than was captured. Sharing unattributed gifts across campaigns in proportion, or by a rule of thumb, would produce a cleaner-looking report that is partly fiction, and campaign decisions would then be made on that fiction.

Campaign pagesone per appeal, configuration only
Campaign recordsa goal, a page and an attribution tag
Attribution at capturethe campaign travels with the gift
Shared gift corewebhook, idempotency and reconciliation, written once
One ledger of giftsevery campaign total is computed from here
Add a campaign and only the top two layers grow.
FigureWhere variety lives and where it is not allowed. Everything above the gift core can differ from one campaign to the next. Everything from the core down exists exactly once, which is why every campaign shares the same reliability and the same totals.
How it was measured

A single headline number hides where a system fails. This work was scored on the dimensions that actually decide whether it holds in production, measured on real, held-out cases rather than the demo path.

Reliability consistency across campaignsAttribution correctness at captureAgreement between campaign rollups and the ledgerEffort to launch an additional campaign
The gate
Did this gift arrive carrying its campaign context?
Credited to that campaign
a recorded fact, captured when the gift was created
Reported as unattributed
visible in the reports, never apportioned
Either way the gift counts once in the ledger, and the campaign totals still add up.
FigureThe rule every gift passes through on its way into the reports. A gift carrying campaign context is credited to that campaign. A gift without it is reported as unattributed and never shared out by a guess.
figures

No measured figures are published for this engagement. Outcomes are described qualitatively.

What it produces
Without this discipline

A donation flow copied per campaign, where a reliability fix lands in one copy, the others drift, and the campaign totals do not add up to the ledger.

This system

One reliable gift core with campaigns as data, attribution captured at the moment of the gift, and per-campaign reporting computed from the same record the gifts were written to.

one core, many campaignsattribute at capturecampaigns are configurationrollups are projections
The operating envelope

What it owns, and what it hands to a person.

Handled with confidence
Many concurrent campaigns over one core
Attribution captured at the gift
Per-campaign reporting from the shared ledger
Flagged for review
Gifts arriving with no campaign context
Out of scope by design
Retrospective attribution of untagged traffic
The honest limit

Attribution is only as good as the tag captured with the gift. A gift that arrives with no campaign context goes into a default bucket and is reported as unattributed. It is not shared out among campaigns by a rule of thumb, because that would make the report look more complete than the knowledge behind it.

What it generalizes to

When a product needs breadth on top of something that has to be reliable, keep the reliable part single and turn the varying part into data. Record where something came from at the moment it happens, because nothing reconstructed afterwards is as trustworthy.

What to take from this

When a product needs many variants of something that has to be reliable, make the variants data over one shared core. Every copy of a critical code path is another place a fix can fail to reach.

Keep totals as computations over a record of individual events. A stored counter cannot explain itself when it is wrong, and so it cannot be corrected with confidence.

Record where something came from at the moment it happens. Attribution reconstructed later from traffic data is a guess, however precise the report looks.

An honest unattributed line in a report is worth more than a complete-looking report built on apportioning.

If the sum of the parts can disagree with the whole, the parts and the whole were computed from different records. Derive both from the same one.

How we engage

You have a system like this one.
Tell us where it stands.

Whether it is failing, not yet built, or about to meet a scale it has never seen, we can tell you what we see.

Start a conversation
mostafa@opulion.dev · Response within 24 hours · By inquiry