[{"data":1,"prerenderedAt":36},["ShallowReactive",2],{"blog-post-three-questions-before-i-write-billing-code":3},{"slug":4,"title":5,"description":6,"date":7,"updated":8,"tags":9,"pillar":12,"draft":13,"image":14,"imageAlt":15,"html":16,"markdown":17,"faq":18,"readingTimeText":34,"readingTimeMinutes":35},"three-questions-before-i-write-billing-code","Three questions I ask before writing a line of a founder's billing code","A three-question framework founders can steal before building billing: what is the source of truth, what happens when this runs twice, and how will we reconstruct it later. Each one maps to a specific expensive failure.","2026-05-27T00:00:00.000Z",null,[10,11,12],"billing","subscriptions","founder-engineering",false,"/blog-covers/founder-engineering.png","Founder engineering cover","\u003Cdiv class=\"post-tldr\">\u003Cdiv class=\"post-tldr-label\">TL;DR\u003C/div>\u003Cp>Before writing billing code, answer three questions: what is the source of truth, what happens when this runs twice, and how will we reconstruct it later. They prevent drift, double charges, and disputes you cannot win, and cost an afternoon.\u003C/p>\u003C/div>\n\u003Cp>Before I write any billing code, I ask three questions: what is the source of truth, what happens when this runs twice, and how will we reconstruct what happened later. Each one is unglamorous. Each one has saved a founder from a support queue full of double charges and disputes they could not explain. If you steal nothing else from me, steal these three, because the cost of answering them is an afternoon and the cost of skipping them is measured in refunds and trust.\u003C/p>\n\u003Cp>I build subscriptions, licensing, and payment flows for a living, mostly as Laravel apps and WordPress plugins. The bugs that hurt are almost never in the happy path. They live in the assumptions nobody wrote down. These questions drag the assumptions into the open before they cost anything.\u003C/p>\n\u003Ch2 id=\"question-one%3A-what-is-the-source-of-truth%3F\" tabindex=\"-1\">Question one: what is the source of truth?\u003C/h2>\n\u003Cp>Pick one system that is allowed to be right, and make everything else defer to it. For money, that is almost always your payment provider, not your database. Your database is a cache of what the provider told you. When they disagree, the provider wins and you reconcile.\u003C/p>\n\u003Cp>Founders get this backwards constantly. They treat the local \u003Ccode>subscriptions\u003C/code> table as the truth and the provider as a thing they sync to occasionally. Then a webhook gets dropped, the local row says “active”, the provider says “canceled for nonpayment”, and the customer keeps using a paid feature for free while your revenue report lies to you.\u003C/p>\n\u003Cp>Deciding the source of truth up front changes the code you write. It means every local write to a billing field is downstream of a provider event, not a user action. It means you build a reconciliation job on day one, not after the first drift incident. And it means when someone asks “is this customer actually paying us”, you have a defensible answer instead of a guess.\u003C/p>\n\u003Ch2 id=\"question-two%3A-what-happens-when-this-runs-twice%3F\" tabindex=\"-1\">Question two: what happens when this runs twice?\u003C/h2>\n\u003Cp>Assume every billing operation will execute more than once, because eventually it will. Webhooks retry. Users double click. A queue redelivers a job after a worker dies mid task. These are the \u003Ca href=\"/blog/six-payments-edge-cases-that-bite-at-2am\">payment edge cases that bite at 2am\u003C/a>. If running the same operation twice charges the card twice or grants the license twice, you have a bug that only appears under load, which is to say in front of your most active customers.\u003C/p>\n\u003Cp>The fix is idempotency, and it is cheaper than the bug. Give every operation a key and let the database reject the duplicate:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// The unique constraint is the actual protection. A check-then-act in\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// application code is a race; the database index is not.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">Schema\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">::\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">create\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'billing_events'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">function\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">Blueprint\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $table) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $table\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">id\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $table\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">string\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'idempotency_key'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">unique\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $table\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">string\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'type'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $table\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">json\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'payload'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $table\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">timestamps\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">});\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>When you call the provider, pass your own idempotency key too. Stripe and most serious processors accept one and will return the original result instead of creating a second charge. That single header turns a scary retry into a safe no-op:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">$stripe\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">charges\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">create\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    [\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'amount'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2900\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'currency'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'usd'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'customer'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $id],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    [\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'idempotency_key'\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"invoice-{\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">$invoiceId\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ask this question before writing the code and idempotency is a design decision. Ask it after an incident and it is a migration under pressure while customers are being double charged.\u003C/p>\n\u003Ch2 id=\"question-three%3A-how-will-we-reconstruct-this-in-six-months%3F\" tabindex=\"-1\">Question three: how will we reconstruct this in six months?\u003C/h2>\n\u003Cp>Every billing state change should leave an append-only trail: what changed, when, who triggered it (a record only as trustworthy as \u003Ca href=\"/blog/authentication-mistakes-in-early-products\">the authentication behind it\u003C/a>), and the value before and after. Not a status column you overwrite. A log you add to. Because six months from now a customer will dispute a charge, a founder will ask why revenue dipped, or you will need to prove to a processor that a cancellation happened when you say it did. Without the trail, all three end in “we are not sure”, which on billing means you eat the cost.\u003C/p>\n\u003Cp>This is also where dunning lives, the \u003Ca href=\"/blog/subscriptions-are-licensing-that-bills-on-time\">retry-and-notify cycle that subscriptions live or die on\u003C/a>. When a card fails and you retry over several days, you want a record of every attempt, every email, and every outcome. When a customer says “you never told me my card failed”, the log either backs them up or clears you. Either way you know, and knowing is the whole point.\u003C/p>\n\u003Cp>The habit that pays off is writing the log entry in the same transaction as the change itself. If the change commits, the record commits. If it rolls back, so does the record. They can never disagree, because they are the same write.\u003C/p>\n\u003Ch2 id=\"why-these-three-and-not-ten\" tabindex=\"-1\">Why these three and not ten\u003C/h2>\n\u003Cp>There are more good billing questions. These three are the ones I refuse to start without, because each maps to a specific, expensive failure mode. Source of truth prevents silent drift between you and the money. Idempotency prevents double charges. The audit trail prevents unwinnable disputes.\u003C/p>\n\u003Cp>Every other billing best practice is easier to add later. These three are painful to retrofit because they shape the schema and the transaction boundaries, which is exactly the stuff you do not want to rewrite once real customers depend on it.\u003C/p>\n\u003Ch2 id=\"use-them-as-a-founder%2C-not-just-as-an-engineer\" tabindex=\"-1\">Use them as a founder, not just as an engineer\u003C/h2>\n\u003Cp>You do not need to write the code to ask the questions. Sit with whoever owns billing, ideally \u003Ca href=\"/blog/the-first-hire-who-touches-your-billing-should-be-boring\">the boring, reliable engineer you hired for exactly this\u003C/a>, and ask all three out loud. Listen for whether the answers are specific. “The provider is the source of truth and we reconcile nightly” is a real answer. “The database, mostly” is a warning. “What do you mean, run twice” is a reason to slow down before launch.\u003C/p>\n\u003Cp>The three questions take an afternoon to answer honestly and they front load the decisions that are almost impossible to change once money is flowing. Billing rarely fails loudly at first. It drifts, it double charges one customer, it loses one dispute, and then one day the numbers stop making sense. These questions are how you make sure that day never comes, or at least that when something breaks, you can see exactly what broke and fix it before your customers do the seeing for you.\u003C/p>\n\u003Cdiv class=\"post-takeaway\">\u003Cdiv class=\"post-takeaway-label\">The takeaway\u003C/div>\u003Cp>These three questions are not a style preference, they are the decisions that are almost impossible to change once real money is flowing. Ask them out loud before launch, whether you write the code or just own the roadmap, and listen for specific answers rather than “mostly” and “what do you mean”. Do that and billing failures become things you can see and fix, instead of a slow drift that surprises you months later.\u003C/p>\u003C/div>\u003Ch2 id=\"faq\" tabindex=\"-1\">FAQ\u003C/h2>\n\u003Cdiv class=\"post-faq\">\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Which of the three questions matters most if I only have time for one?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Source of truth, because getting it wrong makes every other billing number suspect. Decide the provider is authoritative and reconcile against it, and the rest of the system has something honest to lean on.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Why is the payment provider the source of truth and not my database?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Because the provider is where the money actually moves; your database is a cache of what it told you. When they disagree, the provider is right and you reconcile to it.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">How much extra work is idempotency really?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>A unique constraint and an idempotency key passed to the provider, so a scary retry becomes a safe no-op. It is far cheaper than the migration you would run under pressure after customers get double charged.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">What belongs in the audit trail?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>What changed, when, what triggered it, and the value before and after, written in the same transaction as the change itself. That includes dunning: every retry, every email, and every outcome.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">I already launched without these. Is it too late?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>No, but expect it to hurt, because these three shape schema and transaction boundaries. Add reconciliation and the audit trail first, then retrofit idempotency where double execution is most expensive.\u003C/p>\u003C/div>\u003C/div>\u003C/div>","\n## TL;DR\n\nBefore writing billing code, answer three questions: what is the source of truth, what happens when this runs twice, and how will we reconstruct it later. They prevent drift, double charges, and disputes you cannot win, and cost an afternoon.\n\nBefore I write any billing code, I ask three questions: what is the source of truth, what happens when this runs twice, and how will we reconstruct what happened later. Each one is unglamorous. Each one has saved a founder from a support queue full of double charges and disputes they could not explain. If you steal nothing else from me, steal these three, because the cost of answering them is an afternoon and the cost of skipping them is measured in refunds and trust.\n\nI build subscriptions, licensing, and payment flows for a living, mostly as Laravel apps and WordPress plugins. The bugs that hurt are almost never in the happy path. They live in the assumptions nobody wrote down. These questions drag the assumptions into the open before they cost anything.\n\n## Question one: what is the source of truth?\n\nPick one system that is allowed to be right, and make everything else defer to it. For money, that is almost always your payment provider, not your database. Your database is a cache of what the provider told you. When they disagree, the provider wins and you reconcile.\n\nFounders get this backwards constantly. They treat the local `subscriptions` table as the truth and the provider as a thing they sync to occasionally. Then a webhook gets dropped, the local row says \"active\", the provider says \"canceled for nonpayment\", and the customer keeps using a paid feature for free while your revenue report lies to you.\n\nDeciding the source of truth up front changes the code you write. It means every local write to a billing field is downstream of a provider event, not a user action. It means you build a reconciliation job on day one, not after the first drift incident. And it means when someone asks \"is this customer actually paying us\", you have a defensible answer instead of a guess.\n\n## Question two: what happens when this runs twice?\n\nAssume every billing operation will execute more than once, because eventually it will. Webhooks retry. Users double click. A queue redelivers a job after a worker dies mid task. These are the [payment edge cases that bite at 2am](/blog/six-payments-edge-cases-that-bite-at-2am). If running the same operation twice charges the card twice or grants the license twice, you have a bug that only appears under load, which is to say in front of your most active customers.\n\nThe fix is idempotency, and it is cheaper than the bug. Give every operation a key and let the database reject the duplicate:\n\n```php\n// The unique constraint is the actual protection. A check-then-act in\n// application code is a race; the database index is not.\nSchema::create('billing_events', function (Blueprint $table) {\n    $table->id();\n    $table->string('idempotency_key')->unique();\n    $table->string('type');\n    $table->json('payload');\n    $table->timestamps();\n});\n```\n\nWhen you call the provider, pass your own idempotency key too. Stripe and most serious processors accept one and will return the original result instead of creating a second charge. That single header turns a scary retry into a safe no-op:\n\n```php\n$stripe->charges->create(\n    ['amount' => 2900, 'currency' => 'usd', 'customer' => $id],\n    ['idempotency_key' => \"invoice-{$invoiceId}\"]\n);\n```\n\nAsk this question before writing the code and idempotency is a design decision. Ask it after an incident and it is a migration under pressure while customers are being double charged.\n\n## Question three: how will we reconstruct this in six months?\n\nEvery billing state change should leave an append-only trail: what changed, when, who triggered it (a record only as trustworthy as [the authentication behind it](/blog/authentication-mistakes-in-early-products)), and the value before and after. Not a status column you overwrite. A log you add to. Because six months from now a customer will dispute a charge, a founder will ask why revenue dipped, or you will need to prove to a processor that a cancellation happened when you say it did. Without the trail, all three end in \"we are not sure\", which on billing means you eat the cost.\n\nThis is also where dunning lives, the [retry-and-notify cycle that subscriptions live or die on](/blog/subscriptions-are-licensing-that-bills-on-time). When a card fails and you retry over several days, you want a record of every attempt, every email, and every outcome. When a customer says \"you never told me my card failed\", the log either backs them up or clears you. Either way you know, and knowing is the whole point.\n\nThe habit that pays off is writing the log entry in the same transaction as the change itself. If the change commits, the record commits. If it rolls back, so does the record. They can never disagree, because they are the same write.\n\n## Why these three and not ten\n\nThere are more good billing questions. These three are the ones I refuse to start without, because each maps to a specific, expensive failure mode. Source of truth prevents silent drift between you and the money. Idempotency prevents double charges. The audit trail prevents unwinnable disputes.\n\nEvery other billing best practice is easier to add later. These three are painful to retrofit because they shape the schema and the transaction boundaries, which is exactly the stuff you do not want to rewrite once real customers depend on it.\n\n## Use them as a founder, not just as an engineer\n\nYou do not need to write the code to ask the questions. Sit with whoever owns billing, ideally [the boring, reliable engineer you hired for exactly this](/blog/the-first-hire-who-touches-your-billing-should-be-boring), and ask all three out loud. Listen for whether the answers are specific. \"The provider is the source of truth and we reconcile nightly\" is a real answer. \"The database, mostly\" is a warning. \"What do you mean, run twice\" is a reason to slow down before launch.\n\nThe three questions take an afternoon to answer honestly and they front load the decisions that are almost impossible to change once money is flowing. Billing rarely fails loudly at first. It drifts, it double charges one customer, it loses one dispute, and then one day the numbers stop making sense. These questions are how you make sure that day never comes, or at least that when something breaks, you can see exactly what broke and fix it before your customers do the seeing for you.\n\n## The takeaway\n\nThese three questions are not a style preference, they are the decisions that are almost impossible to change once real money is flowing. Ask them out loud before launch, whether you write the code or just own the roadmap, and listen for specific answers rather than \"mostly\" and \"what do you mean\". Do that and billing failures become things you can see and fix, instead of a slow drift that surprises you months later.\n\n## FAQ\n\n**Which of the three questions matters most if I only have time for one?**\nSource of truth, because getting it wrong makes every other billing number suspect. Decide the provider is authoritative and reconcile against it, and the rest of the system has something honest to lean on.\n\n**Why is the payment provider the source of truth and not my database?**\nBecause the provider is where the money actually moves; your database is a cache of what it told you. When they disagree, the provider is right and you reconcile to it.\n\n**How much extra work is idempotency really?**\nA unique constraint and an idempotency key passed to the provider, so a scary retry becomes a safe no-op. It is far cheaper than the migration you would run under pressure after customers get double charged.\n\n**What belongs in the audit trail?**\nWhat changed, when, what triggered it, and the value before and after, written in the same transaction as the change itself. That includes dunning: every retry, every email, and every outcome.\n\n**I already launched without these. Is it too late?**\nNo, but expect it to hurt, because these three shape schema and transaction boundaries. Add reconciliation and the audit trail first, then retrofit idempotency where double execution is most expensive.\n",[19,22,25,28,31],{"q":20,"a":21},"Which of the three questions matters most if I only have time for one?","Source of truth, because getting it wrong makes every other billing number suspect. Decide the provider is authoritative and reconcile against it, and the rest of the system has something honest to lean on.",{"q":23,"a":24},"Why is the payment provider the source of truth and not my database?","Because the provider is where the money actually moves; your database is a cache of what it told you. When they disagree, the provider is right and you reconcile to it.",{"q":26,"a":27},"How much extra work is idempotency really?","A unique constraint and an idempotency key passed to the provider, so a scary retry becomes a safe no-op. It is far cheaper than the migration you would run under pressure after customers get double charged.",{"q":29,"a":30},"What belongs in the audit trail?","What changed, when, what triggered it, and the value before and after, written in the same transaction as the change itself. That includes dunning: every retry, every email, and every outcome.",{"q":32,"a":33},"I already launched without these. Is it too late?","No, but expect it to hurt, because these three shape schema and transaction boundaries. Add reconciliation and the audit trail first, then retrofit idempotency where double execution is most expensive.","7 min read",7,1785070532920]