Case study
Repairing a Norwegian property portal
The portal stayed online while some property updates silently failed. I changed how it handled failed fetches and reduced unnecessary API calls so updates could be retried within the source system's limits.
This case study describes the documented stage of the work, rather than the system's current state. The client is anonymised.
- Work covered
- Nearly seven years of maintenance and development
- Properties at the measurement point
- 101,491
- Technology after modernisation
- PHP 8.4, Symfony 7.4, MySQL 8.4
- Key decision
- Stop acknowledging failed updates as successful
A portal that depends on other systems
Sellers and buyers used the portal to see their property documents, offers, listing statistics and conversations with their agent. Much of that information came from two external systems used by estate agencies.
The portal held a copy of that data, synchronised in the background, and added its own features. It used AWS queues and file storage, with a managed relational database.
My role covered maintaining and developing the existing application. The work described here took place while the portal continued serving users.
A database upgrade exposed missing indexes
The production database moved from MySQL 8.0.44 to 8.4.9 through a Blue/Green switchover to a synchronised replacement. The upgrade completed without downtime. Post-upgrade checks then exposed two slow queries.
The first looked up a property by its identifier in the source system. That field was second in an existing composite index, which did not provide an efficient lookup for this query. The execution plan scanned about 23,500 rows.
The second retrieved a signed-in seller's properties. The seller table contained 158,429 rows but had no index on email or phone. Finding one user required scanning the whole table. That scan accounted for 50 ms of the query's 70 ms execution time.
I added indexes on the running database using a mode that allowed concurrent writes. The changes took 5.1 and 11.3 seconds and required no maintenance window. This addressed the existing database structure; the findings did not establish a defect in the new MySQL version.
An available portal with stale data
An external CRM began marking large numbers of properties as changed while also throttling requests. In one day, 7,739 messages entered the queue instead of the usual few hundred, and requests started returning HTTP 429.
The portal remained available, but 558 failed updates that day were recorded as completed. The fetch code caught HTTP errors without passing the failure back to the queue handler. It logged success and removed the message without retrying.
Data could stay stale until a later source change triggered a successful fetch or someone repaired it manually. The problem surfaced when a buyer-data change failed to reach the portal. Availability monitoring alone had not revealed it.
The decision: a failed fetch must remain a failed update
The repair started by allowing fetch errors to reach the queue handler. A message whose update failed could then be retried instead of being acknowledged as complete.
Retries create more requests, so error handling alone was not enough. I also reduced avoidable calls to leave room within the provider's limits.
- Cached the authentication token. Authentication had accounted for roughly half the CRM traffic.
- Added HTTP retries that respected Retry-After, so a single rate-limit response did not immediately abandon the entire update.
- Corrected date parsing to accept fractional seconds from the CRM. The old parser had prevented queue deduplication from working.
Measured results
| Measurement | Before | After |
|---|---|---|
| Property lookup by source identifier | 130 ms | 0.04 ms |
| Signed-in seller's property-list query | 70 ms | 3.8 ms |
| API calls per property update | About 45 | About 17 |
| Requests to the source system per hour | About 11,000 | About 3,000 |
The first two rows measure individual SQL queries on production MySQL 8.4.9 before and after indexing, not full page-load times. The API figures are approximate measurements from synchronisation logs. Fewer calls reduced the risk of hitting the provider's request limit.
Other problems found during maintenance
The work also uncovered operational issues that affected deployments and everyday use:
- A deployment ran out of disk space because a log had grown to 12 GB without rotation and was also copied into the Docker build.
- 1,586 phone numbers were missing the Norwegian country code. Some were interpreted as valid numbers in other countries.
- Large PDFs opened in the browser instead of downloading. Adding Content-Disposition to signed storage URLs corrected the download behaviour.
The three issues listed here were fixed and the changes deployed. Phone numbers were corrected by a migration after backing up the affected tables.
What remained at the end of this stage
Reducing approximately 45 calls per update to 17 left further work possible. Fetching only changed sections was estimated to reduce that to 3–6 calls; this was a projection requiring verification after implementation.
The portal still polled the CRM periodically. Moving to source-system change notifications remained a separate integration task.
Automatic alerts for rate-limit responses and a growing failed-message queue were still missing. The failed queue contained 2,639 messages needing review. That was not a count of distinct properties or necessarily unresolved updates: duplicates and later successful fetches had to be checked.
What this changed for the business
Failed data fetches became visible failures that could be retried. Reducing API traffic made those retries less likely to exhaust the provider's allowance. The two measured database queries also became faster.
These changes improved the existing system while it served users. The case illustrates why checking data freshness and failure handling matters alongside checking whether a website is online.