Senior Software Engineer

Backpackerlist - Rebuilding a Candidate Search API from 2,000 Lines to 100

Rewriting a job portal's core candidate-listing API — a 2,000+ line tangle of nested if/else filter logic powering both a list view and a live map view — into a ~100-line Eloquent query builder that fixed an N+1 query problem and added real pagination.

2,000+ → ~100

Filter logic LOC

N+1 → fixed

Queries per listing request

Added

Pagination

2

Views unified on one query

Problem

Backpackerlist's candidate search endpoint had grown into a 2,000+ line procedural block of nested conditionals handling every filter combination (location, job category, interests, availability, and more) by hand, duplicated across two nearly identical endpoints — one for the standard list view, one for showing candidates on a Google Map. The API had no real pagination, loaded every matching candidate's related data in a loop, and was becoming harder to safely extend every time a new filter was added.

Challenges

  • Untangling 2,000+ lines of nested if/else filter logic without changing the actual filtering behavior recruiters relied on
  • Two separate endpoints (list view and map view) had drifted out of sync despite needing to return equivalent filtered results
  • An N+1 query problem was firing on every request as related candidate data was fetched in a loop instead of eagerly
  • The listing had no pagination — every filtered request returned the full result set in one response
  • Replacing the filter logic without regressing edge-case filter combinations that had accumulated over time

Architecture

Replaced the hand-rolled conditional chains with a single Eloquent query builder pipeline: each filter (location, job category, interests, availability) maps to a scoped query clause that's conditionally applied only when that filter is present in the request, rather than branching through nested if/else blocks. The list view and map view now share the same underlying query builder and only diverge in the final response shape, eliminating the drift between them. Relations that were previously being lazy-loaded inside loops were converted to eager loads, collapsing what had been one query per candidate into a small, fixed number of queries per request.

1

Filter request parameters mapped to a chain of conditional Eloquent query scopes instead of nested if/else branches

2

List view and map view endpoints unified onto the same underlying query builder, diverging only at response formatting

3

Related models switched from lazy-loaded, per-row queries to eager loading (with()) to eliminate the N+1 pattern

4

Pagination added to the response, replacing the previous full-result-set behavior

5

Infinite-scroll trigger on the frontend calls the next page automatically as the user reaches the end of the list

Database Design

Candidate, location, job-category, and interest data were already relational but were being queried independently per candidate. Formalizing the Eloquent relationships (belongsTo/belongsToMany where appropriate) let the query builder eager-load everything a listing needed in a small, fixed number of queries regardless of result size, instead of query count scaling with the number of candidates returned.

Scalability

Because filters are now composable query scopes rather than branching logic, adding a new filter became a small, isolated addition instead of a risk to the existing conditional tree. Pagination plus eager loading meant response time and query count no longer scaled with the total number of matching candidates, which mattered as the candidate pool grew.

Outcome

Cut the core listing logic from 2,000+ lines to roughly 100 by replacing nested conditionals with composable query scopes, eliminated the N+1 query pattern via eager loading, and shipped pagination with auto-loading infinite scroll on both the list and map views.

Tech Stack

LaravelPHPEloquent ORMMySQLJavaScript

Have a similar problem you're trying to architect your way through?