Hijrah Backend (1): Day One, Handed a Legacy of 370+ Endpoints



The migration story began before I officially joined the company.

At a GPS tracking company in Surabaya, Indonesia, one of the team’s seniors received a mandate to rewrite the existing system. To this day I don’t know exactly what was said in that internal meeting; I only know the outcome: they decided to move to Go. And only after that decision was final did the job opening appear.

So my position in the hiring process was a little unusual: I wasn’t there to help choose the project’s direction. I was there to carry out a decision other people had already made.

At the interview, the main question was fair: could I handle a Laravel-to-Go migration? There was even a test offer, something like “we’ll show you a sample of our app, see if you can work on it within a week”. The offer never materialized. Oh well, the interview went on as usual, and I got the job.

Now, there was one agreement already waiting for me before my first day (who came up with it and when, I have no idea): the decision-makers had settled that this migration project must be complete, development plus testing, within three months.

Three months. For 370+ endpoints.1 Ehem.

That number came from a belief that sounded reasonable in the meeting: migrating Laravel to Go is fast and easy, just move it over and it goes straight to production. Export-import, right? Nobody asked why the one person in that meeting who actually knew the work, whose job was to nod along, didn’t interrupt with a single sentence: “wait, these are different categories”.

That works out to roughly four endpoints a day, seven days a week, no weekends or national holidays. There wasn’t time in the estimate to ask “how did that number get set so confidently?” either. Pretty on a slide, less pretty on a keyboard.

This is the first episode of Hijrah Backend, my own account of moving a GPS backend from PHP to Go. The original deadline is long behind us. I covered the technical side (why Go, the approach, and what I learned) in my April article. This series is about what didn’t fit in that write-up: the decisions, the mistakes, and the people behind the migration.

The Team Structure (and Its Proportions) #

In early 2025, the IT team split three ways: two colleagues maintaining the existing app, two building the admin backoffice, and one dedicated migration sub-team. The migration sub-team had two members: one senior holding the mandate, and me.

The workload was… not exactly balanced.

In practice, I wrote every line of Go code in this series. The mandate holder’s contribution was mostly questions: “why is it erroring?”, “why is the response different?”, “why is it slower? Go is supposed to be faster.” Fair questions, all of them. The person asking just wasn’t the one opening the editor.

When the speed issue got loud, I once countered with a technical note: calling it a “Laravel to Go migration” was never quite apples to apples. Laravel is a framework; Go is a language. To compare frameworks fairly, compare Laravel with CodeIgniter. To compare languages fairly, compare plain PHP with plain Go. Our mandate took the language from one side and the framework from the other, then demanded the speed hehe.

The Inheritance #

The system I received turned out to be two PHP worlds living side by side.

World one: a Laravel 8 API with roughly 290 endpoints, which becomes 370+ if you count the cron jobs.1 World two: a CodeIgniter 3 app that served as the front door for data, a TCP listener receiving packets from dozens of GPS tracker types (Teltonika, GT06, JT808, and dozens of their cousins), plus 100+ cron jobs processing all of it every night.

The data was no joke either. Tens of thousands of GPS units live-tracking, tens of millions of rows of location and alert data, all flowing into MySQL.

The symptoms showed up in the first few weeks. On the fuel-estimation dashboard, for example, 30 vehicles triggered hundreds of queries: fetch each device, fetch its type, then loop through seven days of daily summaries. The database worked overtime while the user waited with a cup of coffee.

There was also an endpoint that could take 15 seconds per call. I blamed several things before finding the cause. That’s where episode two begins.

The Total Rewrite Temptation #

With an inheritance like that, the first temptation was sweet: “just rewrite everything from scratch. Clean, modern, pleasant to read.”

God forbid. I’ll admit, honestly, it got to me for a moment. But a total rewrite meant gambling with behavior that tens of thousands of users already trust. Behavior that was sometimes odd, sometimes undocumented, but very much their daily reality.

So I took the less glamorous path: parity-first, treating the old code as an executable spec. Each new Go endpoint had to behave identically to the old one, quirks included. Except for quirks clearly unused by the frontend, which got cleaned out only after a lot of second-guessing. Slowly, one endpoint at a time, while the old system kept serving. Business as usual.

This decision shaped every episode that follows. Why parity-first saved so much time (plus examples of quirks that made me scratch my head) gets its own episode, number four.

The Map #

The series has eight episodes plus one epilogue. If you’re reading this and the next ones are already out, then everything went live at once, as planned hehe:

  1. Anatomy of a 15-second endpoint: four wrong hypotheses before the real culprit showed up somewhere completely unexpected.
  2. Porting dozens of GPS protocol parsers: replacing the code that reads binary packets from all kinds of trackers, without corrupting data for a live fleet.
  3. Old code as an executable spec: parity-first in depth, what must match exactly, and what’s safe to drop.
  4. Queries that say “use my index”: sargability lessons on tables with tens of millions of rows.
  5. Tidying a monolith into 14 domain modules: restructuring while the plane keeps flying.
  6. From TCP packets to a time-series database: how location data travels from listener to clean storage.
  7. Eighteen months in retrospect: what I’d do differently, plus a Go-teaching plan still waiting for its schedule hehe.
  8. Epilogue: MySQL, PostgreSQL, then MySQL again: the story of a database engine that changed course twice, and who paid the fare.

Closing #

The mandate gave me three months. By the time I wrote this, the migration had long outlasted that deadline. I crossed the distance between the number on the slide and the reality at the keyboard with Allah’s help, my family’s prayers, and my wife’s support. Without them, these notes wouldn’t exist.

Episode two goes back to that 15-second endpoint. Spoiler: the answer wasn’t in the query I suspected.

If you’re looking for a tutorial on building a GPS backend from scratch, I wrote one in the GPS Backend series. Hijrah Backend is the story behind that work.


  1. Around 290 API endpoints in Laravel, plus 100+ cron jobs and endpoints in the CodeIgniter 3 app. The “370+” figure I used in the April article is the sum of both. ↩︎ ↩︎