1. The Lie of "We'll Deal With It When We Scale"
There is a phrase heard more often than any other in software development when deferring work: "We'll deal with it when we scale."
The moment these words are spoken, it is almost certain that the work will never be done.
The reason is simple. When you do scale, you are occupied with entirely different problems that come with scale. User support, bug fixes, feature requests. "That technical debt we deferred" sinks to the bottom of the priority list. And when the limit is truly reached, an ad-hoc patch is applied as an emergency measure.
This is the true nature of "ad-hoc." Deferring itself is not the problem. The problem is not designing a "way back" to the deferred issue.
2. The Difference Between Ad-Hoc and Migration Design
Ad-hoc fixes and migration design appear identical on the surface. Both make the decision to "not do it now."
The difference is a single point.
Migration design makes explicit "what needs to change when the time comes," even though "we're not doing it now."
Specifically, it means the following conditions are met:
- The trigger is defined — what event initiates the migration
- The change surface is localized — what exactly needs to change
- The procedure is documented — how to execute the migration
When these three are in place, "not doing it now" is not negligence but a design decision.
3. Consolidating Into a Single Function
TokiQR's operations send emails through Google Apps Script (GAS). Gmail's free tier allows 100 emails per day. For the early stages of a solo operation, this is sufficient, but as orders grow, the limit will be reached.
In the future, migrating to Amazon SES (Simple Email Service) would provide 62,000 free emails per month. But there is no need to integrate SES now. Orders are still few, and the setup — AWS account, domain verification — can wait until it's needed.
So what was done? All email sending was consolidated into a single function.
function sendEmail(recipient, subject, body, options) {
MailApp.sendEmail(recipient, subject, body, options || {});
}
Fourteen direct calls to MailApp.sendEmail throughout the codebase were replaced to go through this function. When it's time to migrate to SES, only the internals of this one function need to change.
This is not over-engineering. It's adding a single function. But that single function transforms the future migration cost from "rewriting and testing 14 call sites" to "rewriting and testing 1 call site."
4. A Three-Phase Roadmap
The same principle governs not just email sending but TokiQR's entire operational design.
Phase 1: Build it yourself (current)
Printing, UV lamination, packaging, shipping. All done by one person. Operations are fully contained within GAS and Gmail, with zero infrastructure cost. This setup handles up to roughly 50 orders per day.
Phase 2: Domestic outsourcing
Beyond 50 orders per day, laminate production is delegated to a domestic print service. Order data flows from GAS to the vendor, who handles printing, lamination, packaging, and shipping end to end. TokiStorage becomes a fully digital operation with zero inventory and zero manual labor. Email sending migrates to SES.
Phase 3: International expansion
As international orders grow, production is routed to local vendors based on the customer's country. The order data already contains country information, so only routing logic needs to be added.
The code changes required at each phase transition are minimal. Phase 2 requires rewriting the sendEmail function and adding vendor integration. Phase 3 requires adding country-based routing. Neither breaks existing data structures.
Code is not the only thing that changes
What changes during a phase transition is not limited to code.
Outsourcing production means handing customer names and addresses to a third party. The privacy policy must explicitly state the provision of information to subcontractors. The disclosure under the Specified Commercial Transactions Act must be updated to reflect the vendor's lead times. The data handling section of the terms of service requires revision as well.
With the vendor, an NDA must be established defining the scope of customer data handling. An SLA covering production lead times and quality standards must also be agreed upon.
These are not things to "think about when we scale." They are items kept as a list of "what needs updating" right now. The principle of migration design is not limited to code. Legal obligations, contracts, and policies all follow the same rule: write down in advance what needs to change and how. That is the condition for not being ad-hoc.
Automate money in careful stages
TokiQR has a partner revenue-sharing system. For orders placed through referrals, 10% of revenue is paid to the partner. Currently, this is automatically tallied on a monthly basis, and a report with Wise payment links is sent to the administrator. The actual transfer is done manually.
As the number of partners grows, this manual payment will itself become a migration target. But automating money is fundamentally different from automating emails or files. A bug becomes real financial damage instantly.
So the stages are finer-grained. First, auto-generate a batch CSV. The administrator downloads it, uploads it to Wise, and executes a bulk transfer. Because the amounts are reviewed before execution, there is no risk of erroneous payments. The next step is API-based automated transfers — but with a per-transaction cap, requiring administrator approval when exceeded.
The principle of migration design is the same here. "Manual is fine for now. But what to do at the next stage is already written down." Adjusting the granularity of stages according to the magnitude of risk — that, too, is design.
5. The Boundary With Over-Engineering
Migration design carries the temptation of over-engineering. Building abstractions "for the future" that are never used, anticipating every possible scenario.
Where is the boundary?
Add nothing extra to the code that works today.
But separate concerns so that future changes happen in one place.
The sendEmail function, right now, simply calls MailApp.sendEmail. The overhead as a wrapper is effectively zero. But it localizes the future point of change to a single location.
This is the watershed between "migration design" and "over-engineering." It has zero impact on current behavior while reducing future migration cost. What you add is structure, not functionality.
6. Design With Seams
In architecture, there is a concept called an "expansion joint." It is a seam built into a structure so that when an earthquake or temperature change causes the building to expand or contract, the whole structure doesn't crack. In normal times, it does nothing. But when force is applied, that seam protects the building.
Migration design in software works the same way. Function abstraction, data structure separation, configuration externalization. These do nothing in peacetime. But when the phase changes, they absorb the shock of change.
Migration is not about building a massive system in advance.
It is about placing small seams in the right places.
7. The True Cost of Being Ad-Hoc
The cost of ad-hoc fixes is not just technical debt.
The true cost is the degradation of judgment. As emergency responses pile up, the team learns that "it'll be another stopgap anyway." Trust in design erodes, and the option to "build it properly" becomes psychologically distant.
Conversely, in organizations where migration paths are designed, there is no anxiety in the decision to "not do it now." The procedure for when the time comes is visible, so deferral can be done with confidence. Deferral becomes priority management, not guilt.
At TokiQR, the migration procedure is documented in gas/scaling-roadmap.md. The Lambda function code, IAM role configuration, and GAS-side rewrite instructions. Everything is written down. That is why we can say "not now." Because we know exactly what to do when the time comes.
8. Migration in Solo Development
This design principle is not just for large team development. It is, in fact, more essential for solo developers.
On a team, someone might remember the technical debt. But in solo development, you can't even recall what you were thinking three months ago. The "you" who said "we'll deal with it when we scale" no longer exists.
That is precisely why migration paths must be left close to the code. Write the procedure. State the trigger conditions explicitly. Your future self is a stranger. Lay the path now so that stranger can work without hesitation.
Being ad-hoc is not the absence of a path.
It is forgetting that the path ever existed.