A full rewrite is rarely the first step. Start by identifying what holds the product back, whether the current system can be improved, and where a safe change boundary lies.
First decide whether migration is needed.
A language change is not a goal by itself. Name the specific problem: delivery cost, load limits, background job reliability, or maintenance of a particular module. Then compare improving PHP with migration and the long-term cost of supporting two stacks.
01 / OPTION
Keep PHP
If the bottleneck is local and can be fixed through profiling, SQL, caching, or architecture changes, start there.
02 / OPTION
Extract a Go service
When a component has clear boundaries and high load, moving one function can validate the outcome without a large rewrite.
03 / OPTION
Broaden the migration
Consider this only with mapped dependencies, a compatibility budget, and a clear rollout and rollback plan.
A useful first question
What measurable business outcome is needed, and can a smaller change deliver it?
Map the live system.
Before estimating, gather API contracts, client dependencies, data models, background jobs, external integrations, and release constraints. Business rules may also live in queues, scheduled jobs, and error handlers.
- Record current API behavior: statuses, error formats, pagination, ordering, and authorization.
- Identify data ownership and write paths: who changes records, when, and with what side effects.
- Capture a baseline for key workflows: latency, errors, load, and operating cost.
Output of this step
A boundary for the first migration, a list of required contracts, and criteria for validating the result.
Practical worksheet
A worksheet for the first migration slice
A discussion template, not client project data. Fill it in for one function before estimating the move.
Problem and baseline
Which workflow is constrained? Record current behavior, latency, and errors under representative load.
Boundary and data
List inputs, outputs, dependencies, and write ownership. Note what stays in PHP.
Compatibility and rollback
Which API responses must match? How will the new path be enabled and rolled back if needed?
Migrate across a testable boundary.
The safest scope is one that can be enabled, observed, and disabled independently. The specific design depends on the architecture: sometimes a service beside PHP fits; sometimes one workflow can be replaced within the existing product.
This is known as the Strangler Fig pattern: the new implementation gradually takes over workflows until the old one is no longer needed. Before switching, shadow traffic helps: a copy of real requests goes to the new path, its responses are compared with the PHP ones, and clients still get the response from the old system.
01 /
Pick a workflow
Start with a function whose inputs, outputs, and data ownership are clear.
02 /
Lock down compatibility
Cover critical API paths with contract and integration checks.
03 /
Set the data strategy
Decide where records live, how to avoid conflicting updates, and when reads and writes switch.
04 /
Roll out gradually
Compare behavior and metrics on limited traffic; include a switch and rollback path.
Check financial values, dates, locales, and event ordering separately. Identical JSON shape does not guarantee identical behavior for clients.
Where response compatibility breaks.
Contract tests catch schema changes, but PHP and Go serialize the same data differently. Check these differences before the first switch:
- Empty arrays. In PHP,
json_encode([])returns[]both for a list and for an empty associative array where the client expects{}. In Go, a nil slice becomesnull, and an empty map becomes{}. - Missing fields. The
omitemptytag in Go drops fields whose values are0,""orfalse. The PHP response included them, and clients may treat them as required. - Key order. PHP keeps insertion order, while Go sorts map keys alphabetically when encoding. Usually harmless, but it breaks string comparison of responses and clients that rely on order.
- Numbers. In PHP an overflowing integer silently becomes a float, and float precision in JSON depends on
serialize_precision. Money is safer as a string or in minor units. - Dates. PHP takes the time zone from
date.timezone, Go fromtime.Localor an explicit location. Compare the zone and format (DATE_ATOMvstime.RFC3339) on real responses. - Authentication. PHP sessions in files or Redis are stored in PHP’s own format, so a Go service cannot read them directly. Decide up front how Go verifies the user: a shared token (JWT), a check delegated to PHP, or a gateway in front of both services.
A reliable way to find differences is to run the same requests through both paths and compare responses as data structures, not as strings.
Common blind spots in an estimate.
- Dual writes without clear data ownership and conflict rules.
- Hidden dependencies: scheduled jobs, queues, manual operations, and internal API consumers.
- Behavior changes at the edges: null values, time zones, decimals, ordering, and retries.
- A rollout without monitoring or rollback, where users discover failures first.
If these risks are still unknown, a technical review and a small pilot are more useful than an estimate for a full rewrite.
What to bring to the first conversation.
You do not need a finished technical plan. Describe the business goal, the PHP service’s current role, suspected bottleneck, and rollout constraints. Latency charts, an integration diagram, or an API description can help define the first task.
- What needs to improve, and how will the team know it improved?
- Which data and clients must remain compatible?
- Who maintains the system today, and what rollout window is acceptable?