Skip to content
Back to the workshop

Write to the Platform First

When two systems hold the same number, decide which one is the truth

E
EugeneBuilding Cleo
4 min read

Cleo manages Google Ads campaigns. One of the things a user can do is change a campaign's daily budget, by clicking or by asking. For a while, the order of operations on that path was wrong in a way that is easy to write and hard to notice.

The budget change was written to Cleo's own database first. Then it was sent to Google. If Google accepted it, all was well. If Google refused it, or the request never reached Google at all, the local row already said the new number. The screen showed the budget the user had asked for. Google kept spending the old one.

Two copies of one number

Any product that manages another platform holds a copy of that platform's state. Cleo keeps a local record of each campaign so screens load quickly and the AI can reason about the account without a round trip to Google on every turn. That copy is useful. It is also, by definition, a cache. The budget that matters is the one Google is spending against, and Google is the only system that knows it.

Writing the cache before the source turns that relationship upside down. The local row stops being a reflection of the platform and becomes a claim about it. Usually the claim comes true a moment later. When it does not, nothing looks wrong. There is no error on screen, because the write that failed was the second one and the first had already succeeded.

Money paths are the worst place for this. Someone who lowers a budget to stop overspending, sees the lower number confirmed, and gets on with their week has every reason to believe the problem is handled.

Sync first, then record

The fix reverses the order and makes it non-negotiable. A budget change goes to Google first. Only when Google confirms it is the local row updated. If Google refuses, the local row stays as it was, and the user sees Google's own reason for the refusal, translated into a plain sentence, rather than a generic failure.

I put this at the service boundary rather than in the one tool that had the bug. Budget changes arrive from more than one place: a direct request in conversation, an approval queue that carries out changes the owner signed off earlier, and whatever future feature needs to move a budget. Fixing the tool alone would have left the approval queue writing locally while Google spent the old figure. Fixing the service fixed every caller at once, including the ones that do not exist yet.

The same review found a sibling case. An edit that changed a status together with another field did not carry the status change through to the platform. Same shape, same fix.

A guard that returns quietly is not a guard

The second half of this was smaller in code and larger in principle.

Talking to the Google Ads API requires a developer token. Two methods in the Google integration checked for the token and, if it was missing, returned without doing anything. Every neighbouring method threw an error in the same situation. These two returned nothing, and their callers read nothing as success.

So in any environment where the token was absent, a budget change would be skipped silently, and under the old order of operations the local row would already say it had happened. Two quiet behaviours compounded into one convincing false record.

Both guards now throw, matching their siblings. A change that cannot reach the platform must never be recorded as a change that did.

The general rule

When you integrate with a system you do not own, decide early which side is the source of truth for each value, and write in that order. The source first, the copy second, and the copy only on success. If the source refuses, the copy should not move.

And look hard at every early return on a write path. A function that does nothing and reports nothing is indistinguishable from one that did its job, which is exactly why it survives review. Here, the sibling methods that threw were the clue. When one function in a family behaves differently from the others, it is usually the one that is wrong.

E

Written by Eugene

Building Cleo, an AI marketing operating system. These posts cover the architecture decisions, technical challenges, and lessons learned along the way.

More from the workshop