The Savings Plan That Rewrites Itself
A multilingual savings app I built to make myself save. The interesting part isn't the balance creeping up. It's what happens when the plan has to change — a missed deposit, a bigger ambition, a moved date. The schedule rewrites itself, and it never throws the old one away.
I built Akluma for a simple, selfish reason. I wanted to save money, and I wanted something that would hold me to it. It is a Laravel application — PHP and Blade on the back end, Alpine.js and Tailwind CSS on the front — for setting a savings goal and breaking it into a schedule: save this much, this often, and you will have what you need by this date. It speaks three languages — English, Turkish, and French — and it counts in six currencies, including the West and Central African francs used where I live. None of that is the interesting part.
The interesting part is what happens when the plan changes. A savings tracker, drawn on a whiteboard, looks trivial. Add money, watch the number rise. But a savings plan is a promise to your future self, and promises drift. You miss a week. Your situation improves and you want to save faster. The date you were aiming for slips. The moment any of that happens, the tidy schedule you started with is wrong — and the real work of the app begins, quietly, behind a single button, where the user never sees it.
Akluma is also the project that has taught me the most, precisely because I have kept it running for real users over a long time. The lessons in this post are the kind you only learn by maintaining something, not by building it once and walking away.
Five numbers that look alike and aren't
To see how the plan rewrites itself, you first have to meet five numbers Akluma keeps on every goal, all of them columns on a single MySQL table. Three of them never change. Two of them do. I confused them often enough, early on, that I eventually wrote myself a note so I would stop.
frozen at creation:
Price what the thing you want costs
Planned total everything you plan to deposit
Original Goal the end total if you follow the plan
alive afterwards:
Updated Goal empty until the goal actually moves
Still to save recomputed after every change
Three numbers are frozen the moment you create the goal: the price of the thing you are saving for, the planned total of all your deposits, and your Original Goal — the end total you will reach if you follow the plan to the last payment. Those never move again. They are the promise as you first made it, kept on the record exactly as it was.
The other two are alive. One is simply how much you still have to save, recomputed after every deposit and every change. The subtle one is the Updated Goal. It stays empty — literally null in the database — and only takes a value when a recalculation genuinely moves the goalposts. If you merely re-pace the same goal, paying it off faster without changing the total, the Updated Goal stays empty and the screen keeps showing your original one. That sounds pedantic. It is actually the difference between an interface that tells the truth and one that quietly lies to you about what you set out to do.
When the goal moves
So what happens when you change the plan? Say you decide to raise your weekly deposit. You press recalculate, and a Laravel service runs the entire change inside a single MySQL transaction.
First it checks that the change makes sense. The goal is still active. The new amount is positive. There are payments left to reschedule. The new amount is smaller than what you have left to save. Then comes the decision I am most pleased with. The old pending payments are not deleted. They are archived — flagged, set aside, kept on file.
you raise your weekly deposit:
old pending payments → archived (v1), not deleted
new schedule → written fresh (v2)
payments already made → left untouched
( one transaction: all of it commits, or none does )
I chose a plain archived flag over Laravel's soft deletes for a reason I wrote down at the time: I wanted to preserve the old schedule as an audit trail. When a user asks "why did my plan change?", the answer should still exist. With the old payments set aside, the service writes a fresh schedule and stamps every new payment with an incremented version number. Version one was your original plan. Version two is this one. The versions let me trace a goal's whole history, generation by generation, which has rescued me more than once when something looked wrong in production.
Only then does the app decide whether your goal truly moved, fill in the Updated Goal if it did, and recompute what you have left. If anything fails along the way, the transaction rolls back and your old plan is still standing, untouched. The schedule rewrites itself, but there is never a half-rewritten moment in between.
The bug the plan didn't predict
Here is the kind of thing you only find by living with your own code. When I planned this feature, I wrote down that the new payments should keep numbering from the last payment the user had actually completed. It seemed obvious. Continue from where they left off.
It was wrong. Because the old pending payments are archived rather than deleted, they keep their numbers, sitting quietly in the table. Numbering a new schedule from the last completed payment would have collided with those archived rows. So the shipped code does something slightly different from my plan. It takes the highest payment number across every row, archived ones included, and counts up from there. A one-line change, with a comment to my future self explaining why. The plan did not predict it. The running system did.
A smaller discipline runs underneath all of this too. Money is never a float in Akluma. Every amount passes through Brick\Money, a dedicated PHP money library that keeps currency and precision honest. That matters more than usual when six currencies are in play and two of them — the African francs — have no decimal places at all. A rounding shortcut that looks harmless in dollars quietly breaks in francs.
What the project demonstrates
I want to be honest about Akluma's scale. It is a live web application with a few dozen real users, my own savings among them. There is an Android version, but it is a thin wrapper around the website rather than a native app, and it is still in testing — not yet on the Play Store. The reminders that nudge you to save are real, and they go out by email at nine in the morning in each user's own timezone; the push-notification version is designed but not yet built. I would rather tell you exactly what is shipped than dress it up.
None of the stack is exotic, and that is the point. Akluma is a Laravel 12 and PHP application. The front end is Blade and Alpine.js on Tailwind CSS, built with Vite. The data lives in MySQL, the money runs through Brick\Money, and a Pest test suite guards the parts that matter. It deploys to Fly.io on every push through GitHub Actions, across separate staging and production environments. These are boring, proven tools — chosen so I can keep trusting them for years.
What the project demonstrates is not scale. It is care. The engineering worth showing lives in the boring, invisible places. A plan that can change without losing its history. A set of financial numbers that know which of them are allowed to move. A transaction that refuses to leave a half-finished mess. A small bug fixed because I was still around to notice it. Those are the things that decide whether software can be trusted with something as personal as a person's money.
The hard part of a savings app is not adding up money. It is letting a plan change while keeping faith with the plan it used to be — and being honest, on the screen, about which promise you are still keeping.
Akluma is live at akluma.com if you would like to set a goal and watch it hold.
Part of a short series of write-ups on the things I've built. See also The Restaurant Floor That Rearranged Itself, a reservation system that moved its own tables, and The Job Finder That Reads Every Listing, a job search that reads every listing with local language models.