RoomFind
A rental marketplace where the compatibility score decides who is allowed to see a listing at all.
Room share platforms treat a listing as inventory: search, filter by price, call the number. In shared housing the decision people are actually making is about who they will live with, and inventory search has nothing useful to say about that. RoomFind was built for a client in the Irish rental market and runs in production there. I owned the product and data model, the matching, the security, the caching, the deployment, and the console the client runs it from.
In this case study6 sections
01Compatibility as access control
Cold contact from a bad fit never reaches a moderation queue, because the channel to send it never opens.
Every marketplace that computes a match score renders it as decoration. A badge on the card, some influence on sort order, and nothing else in the system depends on it. That leaves both of the real problems in shared housing untouched. Hosts drown in enquiries from people who were never going to work out, and a host renting a room inside their own home has to choose between listing publicly with their address exposed and listing privately to nobody.
RoomFind makes the score an authorization primitive. A seeker has to clear a high compatibility bar before a private listing is visible to them at all, and a lower one before they can message a host without asking first. Below that, the only route to a conversation is to request interest and be accepted. Ranking and permission become the same system.
Cold contact from a bad fit never reaches a moderation queue, because the channel to send it never opens. A host can list privately and still get reach, exclusively to the people the algorithm has already vouched for. The rule is enforced again inside the database, so it holds for anything reaching the data layer without passing through the application. A permission that lives only in application code holds until the first piece of code forgets to ask.
02Weights that change with the listing
Scoring a seeker's cleanliness compatibility against a landlord who lives in another county is noise dressed up as a number.
One weighted average across every attribute produces nonsense on the first edge case. Scoring a seeker's cleanliness compatibility against a landlord who lives in another county is noise dressed up as a number. It also pushes the wrong listings to the top of the feed the whole product depends on.
So the weight vector is a function of the listing type. In a shared flat, lifestyle harmony is the heaviest factor at 35 percent. In a whole apartment it drops to zero and budget doubles to 40 percent, because when nobody is sharing a kitchen with you the only questions left are money, area and dates. Deal breakers short circuit before any weighting, so a listing that forbids pets scores zero for a seeker with a dog regardless of how well the rest lines up.
A seeker with no profile scores null rather than zero, which keeps incompatible and unknown as distinct states and lets the interface ask for a profile instead of reporting that every listing in the country is a zero percent match. Scores then resolve into named bands with their specific reasons attached, because the difference between 74 and 76 is noise, and a number with no reasoning behind it is an algorithm asserting something about a stranger's home.
Ordering the feed is a separate question from permission, answered with compatibility at 70 percent, freshness at 20 and listing completeness at 10. A listing cannot buy its way up by being reposted, because freshness is worth a fifth of what compatibility is worth.
03Ranking before there is data to learn from
A learned ranker needs labelled outcomes to fit on, and on day one there are no users, no interest requests, and nothing to label.
The reasonable objection to hand tuned weights is that a model should be learning them. That has a sequencing problem. A learned ranker needs labelled outcomes to fit on, and on day one there are no users, no interest requests, and nothing to label. Nobody arrives for a product that cannot rank anything, and nothing exists to train on until they do.
Domain knowledge is real information that exists before any data does. Knowing that lifestyle harmony dominates when two people share a kitchen and means nothing when they do not is a prior, and encoding it as weights means the product ranks sensibly for the first user, which is what generates the outcomes a model would later need. Heuristics first, then learned weights over the same features, is how production ranking systems are actually built. The version worth avoiding is paying for a learning pipeline before there is anything to learn from.
04Measuring whether it works
If the bands do not separate, the weights are wrong, and I would rather find that in a chart than in a client's churn.
Weights chosen from domain knowledge are a starting position rather than a conclusion, so the system has to be capable of telling you later that they were wrong. The thresholds were reasoned from product constraints, which is a fine way to pick a first number and a poor way to keep one.
So the funnel is instrumented by match band rather than in aggregate. Impressions, profile opens, listing clicks, interest requests, acceptances, viewing requests, conversations and first replies each carry the band the match fell into and whether it cleared the contact threshold. Seekers above the threshold should convert to accepted interest at a visibly higher rate than seekers below it. If the bands do not separate, the weights are wrong, and I would rather find that in a chart than in a client's churn.
Underneath the product analytics, request identifiers propagate into a sanitized activity log recording level, service, action, status, environment and metadata for every event, so a moderation decision or a failed background job can be reconstructed rather than guessed at. The two answer different questions: whether the product is working, and what the system actually did.
05Making personalized feeds cheap
Every user sees a different feed, and a single profile edit invalidates a large and unenumerable set of derived views.
Personalized ranking is where marketplace performance usually goes wrong. Every user sees a different feed, and a single profile edit invalidates a large and unenumerable set of derived views. Deleting keys requires already knowing every key you would have to delete, and short expiry trades correctness for a guess.
Compatibility scores are precomputed into an indexed table by a batched background service, so a feed load is an indexed read rather than scoring every seeker against every listing on the request path. Cache keys embed a version counter and invalidation increments it, atomically orphaning every derived key in a single write. Each mutation type carries its own key set, so editing a listing leaves every other user's cached feed untouched, and expiry is tiered by how personal the response is.
A scale audit of the listing path then surfaced four things that worked correctly and would have grown expensive with volume, all closed without adding infrastructure. That is the commercial point. Buying a bigger database and a search cluster also works, and it permanently raises the floor on what the platform costs to run, at a stage where the money does more good bringing in the users who would eventually justify it. Architecture should bend to the business.
06Security, and handing it over
Reads can degrade. Writes must never proceed unauthenticated.
The interesting case in authentication is the provider having a bad thirty seconds. A blip should not log a browsing user out, and a write endpoint should not fall through as though nobody had asked who the caller was. Pages degrade, write capable routes refuse outright, and administrative routes fail closed unconditionally. Reads can degrade. Writes must never proceed unauthenticated.
Rate limit counters live in the shared cache, and when it is unreachable production denies while development falls back to memory, because an availability incident should not quietly become an open door. Client identification validates proxy headers as real addresses before trusting them. Form tokens are bound to the session that issued them, so one lifted from another session is inert. Conversation and support attachments sit in private storage, since a link that works for anybody holding it is not a permission model.
Operating the platform is the client's job rather than mine. A platform whose owner has to phone the developer to suspend an abusive account is not finished. The console carries a moderation queue, listing approval, suspension with recorded reasons, role management, support ticketing and analytics, with reauthentication on destructive actions and an audit trail behind privileged ones. Notifications fan out from one call to in app, email and push, so no feature touches a delivery transport directly.
Every push runs the test suites, a production build and lint in parallel, and nothing reaches the branch that deploys without a green build. Building software somebody else operates is a different discipline from building software you operate yourself, and the tell is whether the person paying for it can answer their own questions without you.
