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.
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 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.
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.
The reflex, and the fix.
Fork the donation flow per campaign
Each campaign gets exactly the flow it wants, and nobody has to negotiate a shared abstraction.
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.
One gift core, campaigns as data
A campaign model and an attribution decision made up front, which is slower on the first day than copying a page that already works.
One place where gift reliability lives, and campaigns that are records with a goal, a page and an attribution tag.
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.
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.
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.
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.
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.
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.
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.
No measured figures are published for this engagement. Outcomes are described qualitatively.
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.
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.
What it owns, and what it hands to a person.
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.
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.
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.