How to Design a Real Estate Listing & Search Platform
Building Zillow/Rightmove at Scale — Geospatial Search, ML Price Estimation, Compliance & Full Lifecycle
1. Introduction & The Real Estate Platform Challenge
The real estate industry represents one of the largest asset classes in the world, with residential property transactions alone exceeding $2 trillion annually in the United States. Platforms like Zillow, Rightmove, Redfin, and Realtor.com have fundamentally transformed how people search for, evaluate, and transact on properties. Building such a platform is a formidable engineering challenge that combines geospatial search, machine learning, document management, lead generation, compliance with housing regulations, and real-time collaboration between buyers, sellers, and agents.
At its core, a real estate platform solves a marketplace problem: connecting property seekers with available properties while providing enough data, tools, and trust signals to enable high-value decisions. Unlike typical e-commerce platforms where items are homogeneous and prices are fixed, every property is unique, prices are negotiated, transactions take weeks to months, and the stakes involve life-changing financial commitments. The platform must serve multiple user personas — home buyers, renters, sellers, real estate agents, property managers, and mortgage lenders — each with distinct workflows and data needs.
The technical challenges are substantial. Property search requires geospatial indexing that supports radius queries, polygon containment (draw-on-map), and hierarchical geographic filtering (city → neighborhood → zip code → street). Listing photos and virtual tours demand sophisticated media pipelines with CDN delivery. Price estimation requires ML models trained on millions of comparable sales. And the entire system must comply with Fair Housing Act regulations, ADA accessibility standards, and state-specific real estate disclosure laws.
Real-World Scale & Case Studies
| Platform | Scale | Key Technical Innovation |
|---|---|---|
| Zillow | 110M+ homes tracked, 2B+ page views/month | Zestimate ML model, 3D home tours, instant offers |
| Rightmove | UK's largest property portal, 90%+ market share | Advanced map-based search, school catchment overlays |
| Redfin | 100K+ home tours/year, 30+ markets | Agent matching algorithm, 3D walkthroughs, real-time alerts |
| Realtor.com | 1B+ property data updates/month | Floor plan analysis, neighborhood scoring, off-market leads |
| CoStar/LoopNet | 6B+ sq ft of commercial property tracked | Commercial-specific analytics, tenant tracking, comps engine |
2. Functional & Non-Functional Requirements
Functional Requirements
Property Listings
- Agents/sellers can create, edit, and deactivate property listings with rich structured data (address, price, bedrooms, bathrooms, square footage, year built, lot size, property type, amenities, HOA details)
- Upload multiple high-resolution photos (up to 50 per listing), virtual tours (Matterport/embedded 3D), video walkthroughs, and floor plans
- Automatic photo enhancement, EXIF stripping, and responsive image delivery via CDN at multiple breakpoints
- Listing status lifecycle: Draft → Pending Review → Active → Under Contract → Sold/Rented → Withdrawn → Expired
- Scheduled listing publication and expiration dates, automatic status transitions
Property Search
- Text-based address search with autocomplete powered by geocoding API
- Filter by price range, bedrooms, bathrooms, square footage, lot size, year built, property type (single-family, condo, townhouse, multi-family, land, commercial)
- Map-based search with draw-on-map polygon selection, zoom-level-aware clustering
- Geospatial queries: radius search, bounding box, within school district, within commute time of a workplace
- Advanced filters: open houses only, new construction, waterfront, HOA included, pet-friendly, price reduced
- Sort by price, date listed, square footage, Zestimate accuracy, relevance score
Property Detail Pages
- Comprehensive property overview with photo gallery, virtual tour embed, and 2D floor plan
- Price history chart, tax assessment history, and comparable sales
- Neighborhood insights: school ratings, crime statistics, walkability/transit/bike scores, nearby amenities
- Mortgage calculator with adjustable rate, down payment, and term
- Agent contact form with lead capture, showing scheduling, and favorite/save actions
User Features
- Account creation (buyer, seller, agent personas) with role-based access control
- Saved searches with configurable alert frequency (instant, daily digest, weekly)
- Favorite properties with notes, price change tracking, and sharing
- Showing request scheduling with calendar integration
- Open house RSVP and attendance tracking
Agent & Transaction Features
- Agent profiles with license verification, transaction history, reviews, and specializations
- Inquiry management dashboard with lead scoring and follow-up workflows
- Comparable Market Analysis (CMA) report generation
- Document upload for contracts, disclosures, inspection reports, and appraisals
- Rental application submission, tenant screening (credit check, background check), and lease management
Analytics & Insights
- Market trends dashboard: median prices, days on market, inventory levels, price per sq ft over time
- Neighborhood comparison tools
- MLS data integration for real-time listing syndication
- Agent performance dashboards: lead conversion, listing exposure, response time
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Search latency (p99) | < 200ms | Users expect near-instant results when exploring neighborhoods |
| Detail page load (p95) | < 500ms | High engagement page; slow loads lose buyers |
| Photo delivery (CDN) | < 100ms (cache hit) | Image-heavy pages with 20+ photos per listing |
| Availability | 99.95% | Real estate is 24/7; downtime during weekends is catastrophic |
| Data freshness | < 15 min for MLS sync | Stale listings erode trust |
| Concurrency | 100K concurrent users | Peak traffic during spring buying season, open house weekends |
| Storage | PB-scale photos/videos | 110M homes × 50 photos avg × 5MB = 27.5 PB raw |
| Compliance | Fair Housing Act, ADA, GDPR/CCPA | Legal requirement; violations carry severe penalties |
3. Capacity Estimation & Cost Analysis
Traffic Estimates
Assuming a platform on the scale of a mid-tier regional MLS portal (not Zillow-scale):
- Listings: 2M active listings (US regional), updated 500K times/day via MLS sync and manual edits
- Search queries: 10M searches/day → ~115 QPS average, 500 QPS peak (evenings/weekends)
- Detail page views: 50M pages/day → ~580 QPS average, 2,500 QPS peak
- Photo views: 500M images/day → ~5,800 QPS average, 25,000 QPS peak (served from CDN)
- Lead submissions: 200K/day → ~2.3 QPS average
- User accounts: 5M registered users, 500K monthly actives
Storage Estimates
- Property data (structured): 2M listings × 10KB avg = 20GB (fits in a single Postgres instance)
- Photos: 2M × 30 avg photos × 3MB avg = 180TB raw → ~60TB after resizing to multiple sizes
- Videos/Virtual tours: 200K listings × 50MB avg = 10TB
- User data: 5M users × 5KB = 25GB
- Search indices: ~50GB (PostGIS) + 20GB (Elasticsearch)
- Historical data (tax, sold records): 50M records × 2KB = 100GB
Bandwidth Estimates
- Inbound (MLS sync + user uploads): ~5GB/day structured data + 500GB/day media uploads
- Outbound (CDN-served): ~10TB/day (photos + pages), with 90%+ CDN cache hit ratio reducing origin bandwidth to ~1TB/day
Cost Breakdown (Monthly Estimate)
| Service | Configuration | Monthly Cost |
|---|---|---|
| Application Servers (EKS) | 10 × c6g.xlarge (4 vCPU, 8GB) | ~$1,400 |
| PostgreSQL (RDS Multi-AZ) | db.r6g.2xlarge, 2TB gp3 | ~$1,200 |
| Elasticsearch (OpenSearch) | 6-node cluster, r6g.large | ~$1,500 |
| Redis (ElastiCache) | 3-node cluster, r6g.large | ~$800 |
| S3 Storage | 80TB + CDN | ~$2,000 |
| CloudFront CDN | 10TB/month transfer | ~$900 |
| ML Inference (SageMaker) | 2 × ml.g4dn.xlarge | ~$750 |
| Message Queue (SQS/SNS) | Moderate throughput | ~$100 |
| Monitoring (CloudWatch + Datadog) | Full observability stack | ~$800 |
| Total | ~$9,450 |
4. Data Model & Storage Schema
Core Entities
C#
public class Property
{
public Guid Id { get; set; }
public string MlsNumber { get; set; }
public PropertyType Type { get; set; }
public ListingStatus Status { get; set; }
public decimal ListPrice { get; set; }
public decimal? SalePrice { get; set; }
public DateTime ListedDate { get; set; }
public DateTime? SoldDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string County { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public NetTopologySuite.Geometries.Point GeoPoint { get; set; }
public int Bedrooms { get; set; }
public decimal Bathrooms { get; set; }
public int SquareFeet { get; set; }
public int? LotSizeSqFt { get; set; }
public int YearBuilt { get; set; }
public int? GarageSpaces { get; set; }
public string ConstructionType { get; set; }
public string Heating { get; set; }
public string Cooling { get; set; }
public decimal? HoaFee { get; set; }
public decimal? TaxAssessment { get; set; }
public decimal? EstimatedValue { get; set; }
public Guid ListingAgentId { get; set; }
public Guid? SellingAgentId { get; set; }
public List<PropertyPhoto> Photos { get; set; }
public List<PropertyFeature> Features { get; set; }
public List<PriceHistory> PriceHistories { get; set; }
public List<TaxRecord> TaxRecords { get; set; }
public string NeighborhoodId { get; set; }
public string SchoolDistrictId { get; set; }
}
public class PropertyPhoto
{
public Guid Id { get; set; }
public Guid PropertyId { get; set; }
public string OriginalUrl { get; set; }
public string ThumbnailUrl { get; set; }
public string MediumUrl { get; set; }
public string LargeUrl { get; set; }
public int SortOrder { get; set; }
public PhotoType Type { get; set; }
public string Caption { get; set; }
public bool IsPrimary { get; set; }
public DateTime UploadedAt { get; set; }
}
public class Agent
{
public Guid Id { get; set; }
public string LicenseNumber { get; set; }
public string LicenseState { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string BrokerageName { get; set; }
public string Bio { get; set; }
public string ProfilePhotoUrl { get; set; }
public List<string> Specializations { get; set; }
public List<string> ServiceAreas { get; set; }
public double AverageRating { get; set; }
public int TotalReviews { get; set; }
public int TotalTransactions { get; set; }
public bool IsVerified { get; set; }
}
public class SavedSearch
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; }
public string SearchCriteriaJson { get; set; }
public AlertFrequency AlertFrequency { get; set; }
public bool IsAlertEnabled { get; set; }
public int MatchCount { get; set; }
public DateTime LastCheckedAt { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Inquiry
{
public Guid Id { get; set; }
public Guid PropertyId { get; set; }
public Guid FromUserId { get; set; }
public Guid ToAgentId { get; set; }
public InquiryType Type { get; set; }
public string Message { get; set; }
public InquiryStatus Status { get; set; }
public DateTime? ShowingDateTime { get; set; }
public decimal? LeadScore { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Neighborhood
{
public string Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public string State { get; set; }
public NetTopologySuite.Geometries.Polygon Boundary { get; set; }
public double MedianHomePrice { get; set; }
public double MedianRent { get; set; }
public int AverageDaysOnMarket { get; set; }
public double WalkScore { get; set; }
public double TransitScore { get; set; }
public double BikeScore { get; set; }
public double CrimeRateIndex { get; set; }
public List<SchoolInfo> Schools { get; set; }
public List<AmenityInfo> NearbyAmenities { get; set; }
}
Storage Technology Decisions
| Data Type | Technology | Rationale |
|---|---|---|
| Property records | PostgreSQL + PostGIS | ACID transactions, geospatial indexing, relational integrity for MLS data |
| Search index | Elasticsearch | Full-text search, faceted filtering, geo-bounding-box queries, relevance tuning |
| Photos/videos | S3 + CloudFront | Unlimited storage, CDN delivery, lifecycle policies for cost optimization |
| Sessions/cache | Redis Cluster | Sub-ms latency for session storage, search result caching, rate limiting |
| User data | PostgreSQL | Relational integrity, joins with property data |
| Analytics/events | Apache Kafka → Redshift | Event streaming for click tracking, search analytics, ML feature engineering |
| Document storage | S3 + DynamoDB metadata | Large binary blobs in S3, metadata/indexing in DynamoDB |
5. High-Level Architecture Overview
(React/Next.js)"] MOB["Mobile App
(React Native)"] API_EXT["External MLS
Integrations"] end subgraph Gateway["API Gateway"] GW["API Gateway
(Kong/AWS ALB)
Rate Limiting, Auth, Routing"] end subgraph Services["Microservices"] SEARCH_SVC["Search Service"] LISTING_SVC["Listing Service"] MEDIA_SVC["Media Service"] USER_SVC["User Service"] AGENT_SVC["Agent Service"] LEAD_SVC["Lead & Inquiry Service"] ML_SVC["ML Price Estimation"] NEIGHBORHOOD_SVC["Neighborhood Service"] MORTGAGE_SVC["Mortgage Calculator"] DOC_SVC["Document Management"] NOTIFICATION_SVC["Notification Service"] RENTAL_SVC["Rental Application Service"] end subgraph Data["Data Layer"] PG[("PostgreSQL + PostGIS")] ES[("Elasticsearch Cluster")] REDIS[("Redis Cluster")] S3[("S3 Media Storage")] DDB[("DynamoDB Doc Metadata")] KAFKA["Kafka Event Stream"] end subgraph Infra["Infrastructure"] CDN["CloudFront CDN"] ML_INFRA["SageMaker Endpoints"] MONITOR["CloudWatch + Datadog"] end WEB & MOB & API_EXT --> GW GW --> SEARCH_SVC & LISTING_SVC & MEDIA_SVC & USER_SVC & AGENT_SVC GW --> LEAD_SVC & ML_SVC & NEIGHBORHOOD_SVC & MORTGAGE_SVC & DOC_SVC GW --> NOTIFICATION_SVC & RENTAL_SVC SEARCH_SVC --> ES & REDIS LISTING_SVC --> PG & KAFKA MEDIA_SVC --> S3 & CDN USER_SVC --> PG AGENT_SVC --> PG LEAD_SVC --> PG & KAFKA ML_SVC --> ML_INFRA NEIGHBORHOOD_SVC --> PG & ES DOC_SVC --> S3 & DDB NOTIFICATION_SVC --> KAFKA RENTAL_SVC --> PG
Service Responsibilities
| Service | Responsibility | Key Tech |
|---|---|---|
| Search Service | Query processing, filter application, geo-queries, result ranking, caching | Elasticsearch, Redis, PostGIS |
| Listing Service | CRUD operations, MLS sync, status lifecycle, validation, pricing updates | PostgreSQL, Debezium CDC |
| Media Service | Photo upload, processing pipeline, CDN management, virtual tour embedding | S3, Lambda, CloudFront |
| User Service | Authentication, profiles, preferences, saved searches, favorites | PostgreSQL, JWT, OAuth2 |
| Agent Service | Agent profiles, license verification, reviews, performance metrics | PostgreSQL, 3rd-party license APIs |
| Lead & Inquiry Service | Contact forms, lead scoring, showing requests, follow-up automation | PostgreSQL, ML scoring model |
| ML Price Estimation | Zestimate generation, comparable sales analysis, market trend predictions | SageMaker, feature store |
| Neighborhood Service | School data, crime stats, walkability scores, boundary polygons | PostGIS, 3rd-party APIs |
| Mortgage Calculator | Payment calculations, affordability analysis, lender integration | Math engine, rate APIs |
| Document Service | Contract uploads, disclosures, e-signature integration, lease management | S3, DynamoDB, DocuSign API |
| Rental Service | Rental applications, tenant screening, lease lifecycle | PostgreSQL, screening APIs |
| Notification Service | Email alerts, push notifications, SMS for showing reminders | SES, FCM, SNS |
6. Property Listing Management
Listing management is the backbone of the platform. Listings can originate from three primary sources: MLS data feeds (IDX/RETS/DAML), direct agent input via the web interface, and bulk imports for property managers handling rental portfolios. Each source has different data quality, update frequency, and schema requirements.
MLS Data Ingestion Pipeline
(IDX/RETS)"] --> B["Ingestion Service"] B --> C["Schema Normalization"] C --> D["Validation & Deduplication"] D --> E["PostgreSQL Write"] E --> F["CDC Debezium"] F --> G["Elasticsearch Index Update"] F --> H["Notification Dispatch"] F --> I["Cache Invalidation"]
MLS data arrives in varying formats — RETS (Real Estate Transaction Standard) uses a proprietary protocol, IDX (Internet Data Exchange) feeds are XML/JSON, and newer DAML feeds use modern APIs. The ingestion service must normalize all incoming data into a canonical schema, handle deduplication (same property listed by multiple agents), and manage the update-vs-insert decision logic.
Listing Lifecycle State Machine
C#
public enum ListingStatus
{
Draft, PendingReview, Active, PriceReduced,
UnderContract, Pending, Sold, Rented,
Withdrawn, Expired
}
public class ListingStateMachine
{
private static readonly Dictionary<ListingStatus, HashSet<ListingStatus>> Transitions = new()
{
[ListingStatus.Draft] = new() { ListingStatus.PendingReview },
[ListingStatus.PendingReview] = new() { ListingStatus.Active, ListingStatus.Draft },
[ListingStatus.Active] = new() { ListingStatus.PriceReduced, ListingStatus.UnderContract, ListingStatus.Withdrawn, ListingStatus.Expired },
[ListingStatus.PriceReduced] = new() { ListingStatus.UnderContract, ListingStatus.Withdrawn, ListingStatus.Expired },
[ListingStatus.UnderContract] = new() { ListingStatus.Pending, ListingStatus.Active },
[ListingStatus.Pending] = new() { ListingStatus.Sold, ListingStatus.Rented, ListingStatus.Active },
};
public bool CanTransition(ListingStatus current, ListingStatus target)
{
return Transitions.TryGetValue(current, out var allowed) && allowed.Contains(target);
}
}
Photo Upload Pipeline
The media pipeline handles the entire lifecycle of listing photos from upload through delivery:
- Upload: Client-side generates pre-signed S3 URLs to upload directly to S3, bypassing the application server for large binary transfers. Maximum 50 photos per listing, 25MB per photo.
- Processing: An S3 event triggers a Lambda function that strips EXIF data (privacy), generates 4 sizes (thumbnail 300px, medium 800px, large 1600px, original), converts to WebP format, detects and corrects orientation, and runs content moderation (NSFW detection).
- CDN Distribution: Processed images are placed in a CloudFront-origin bucket. Multiple cache behaviors handle different sizes with aggressive TTLs (30 days for processed images since listing photos are immutable after processing).
- Virtual Tours: Matterport 3D tour embeds are stored as iframe URLs. Video walkthroughs are uploaded to S3 and transcoded via MediaConvert into HLS segments with adaptive bitrate streaming.
Listing Validation Rules
| Field | Validation Rule | Error Handling |
|---|---|---|
| Address | Geocoded to exact coordinates via Google Maps API | Reject if geocoding fails; flag if confidence < 0.8 |
| Price | $10,000 - $100,000,000; must be within 30% of Zestimate | Warn if outlier; require agent justification |
| Square Feet | 100 - 100,000; cross-validate with tax records | Flag discrepancy > 20% for review |
| Photos | Minimum 1 exterior; no duplicates (perceptual hash) | Reject duplicates; require minimum count |
| Bedrooms | 0 - 20; must match floor plan data if available | Flag inconsistency |
| Description | 50-5,000 characters; no discriminatory language | AI scan for Fair Housing violations; reject if flagged |
7. Property Search Engine
Property search is the most complex and performance-critical component of the platform. Users expect sub-second results across millions of listings with a combination of geospatial, numeric, categorical, and text-based filters. The search architecture must support both structured queries (filter by bedrooms and price) and unstructured queries (search for "open concept kitchen with island").
Search Architecture
Elasticsearch Index Mapping
JSON
{
"mappings": {
"properties": {
"mls_number": { "type": "keyword" },
"status": { "type": "keyword" },
"property_type": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"bedrooms": { "type": "byte" },
"bathrooms": { "type": "half_float" },
"square_feet": { "type": "integer" },
"lot_size_sqft": { "type": "integer" },
"year_built": { "type": "short" },
"hoa_fee": { "type": "scaled_float", "scaling_factor": 100 },
"location": { "type": "geo_point" },
"address": {
"type": "text",
"analyzer": "standard",
"fields": {
"autocomplete": { "type": "completion", "analyzer": "simple" }
}
},
"description": { "type": "text", "analyzer": "english" },
"features": { "type": "keyword" },
"school_district": { "type": "keyword" },
"neighborhood": { "type": "keyword" },
"listing_date": { "type": "date" }
}
}
}
Search Query Processing
C#
public class PropertySearchService
{
private readonly IElasticClient _elastic;
private readonly IConnectionMultiplexer _redis;
public async Task<SearchResult> SearchAsync(PropertySearchRequest request)
{
var cacheKey = GenerateCacheKey(request);
var cached = await _redis.GetDatabase().StringGetAsync(cacheKey);
if (cached.HasValue)
return JsonSerializer.Deserialize<SearchResult>(cached);
var searchDescriptor = new SearchDescriptor<PropertyIndex>()
.Index("properties")
.Size(request.PageSize)
.From(request.Page * request.PageSize);
if (request.Latitude.HasValue && request.Longitude.HasValue)
{
searchDescriptor.Query(q => q
.Bool(b => b
.Filter(f => f
.GeoDistance(gd => gd
.Field(p => p.Location)
.Distance(request.RadiusMiles, DistanceUnit.Miles)
.Location(request.Latitude.Value, request.Longitude.Value)
)
)
)
);
}
if (request.MinPrice.HasValue || request.MaxPrice.HasValue)
{
searchDescriptor.Query(q => q
.Range(r => r
.NumberRange(nr => nr
.Field(p => p.Price)
.GreaterThanOr(request.MinPrice)
.LessThanOr(request.MaxPrice)
)
)
);
}
if (!string.IsNullOrEmpty(request.QueryText))
{
searchDescriptor.Query(q => q
.MultiMatch(mm => mm
.Fields(f => f
.Field(p => p.Address, 3.0)
.Field(p => p.Description, 1.0)
.Field(p => p.Neighborhood, 2.0))
.Query(request.QueryText)
.Type(TextQueryType.BestFields)
.Fuzziness(Fuzziness.Auto)
)
);
}
searchDescriptor.Sort(s =>
{
if (request.SortBy == SortOption.Price)
return request.SortDesc ? s.Descending(p => p.Price) : s.Ascending(p => p.Price);
return s.Descending(p => p.ListingDate);
});
searchDescriptor.Aggregations(a => a
.Terms("property_types", t => t.Field(p => p.PropertyType).Size(10))
.Range("price_ranges", r => r.Field(p => p.Price)
.Range("0-200000", 0, 200000)
.Range("200000-400000", 200000, 400000)
.Range("400000-600000", 400000, 600000)
.Range("600000+", 600000, null))
);
var response = await _elastic.SearchAsync<PropertyIndex>(searchDescriptor);
await _redis.GetDatabase().StringSetAsync(cacheKey,
JsonSerializer.Serialize(response), TimeSpan.FromMinutes(5));
return MapToResult(response);
}
}
Map View & Draw-on-Map
The map view renders property markers on a Mapbox/Google Maps canvas, clustering markers at lower zoom levels and showing individual pins as the user zooms in. The draw-on-map feature allows users to draw arbitrary polygons on the map and return only properties contained within the drawn boundary.
For draw-on-map, the client sends the polygon coordinates to the server, which executes a PostGIS ST_Contains query against the property table's geometry column. This is computationally expensive for complex polygons, so we pre-compute a simplified bounding box to narrow the candidate set before executing the exact containment check.
C#
public async Task<List<PropertyListItem>> GetPropertiesInPolygon(
List<(double Lat, double Lng)> polygonPoints)
{
var bbox = CalculateBoundingBox(polygonPoints);
var candidates = await SearchByBoundingBox(bbox.LatMin, bbox.LngMin, bbox.LatMax, bbox.LngMax);
var wkt = $"POLYGON(({string.Join(",",
polygonPoints.Select(p => $"{p.Lng} {p.Lat}"))}))";
return await _context.Properties
.FromSqlRaw($@"
SELECT * FROM properties
WHERE ST_Contains(
ST_GeomFromText('{wkt}', 4326),
geo_point
) AND status = 'Active'
ORDER BY price ASC
LIMIT 500")
.ToListAsync();
}
8. Geospatial Indexing & Map View
Geospatial indexing is the defining technical challenge of a real estate platform. Unlike standard search queries that match text, property search requires efficient spatial operations: finding all properties within a radius of a point, within a bounding box, within a user-drawn polygon, within a school district boundary, or within a specified commute time of a workplace.
PostGIS Spatial Indexing
PostGIS extends PostgreSQL with geographic objects and spatial indexing capabilities. We create a GiST (Generalized Search Tree) index on the geo_point column for efficient spatial queries.
SQL
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER TABLE properties
ADD COLUMN geo_point GEOMETRY(Point, 4326);
CREATE INDEX idx_properties_geo_point
ON properties USING GIST (geo_point);
CREATE INDEX idx_properties_search
ON properties USING GIST (geo_point)
WHERE status = 'Active';
-- Radius query: Find properties within 5 miles
SELECT id, list_price, sqft,
ST_Distance(geo_point, ST_SetSRID(ST_MakePoint(-73.935242, 40.730610), 4326)) * 0.000621371 AS distance_miles
FROM properties
WHERE status = 'Active'
AND ST_DWithin(
geo_point,
ST_SetSRID(ST_MakePoint(-73.935242, 40.730610), 4326),
0.072
)
ORDER BY geo_point <-> ST_SetSRID(ST_MakePoint(-73.935242, 40.730610), 4326)
LIMIT 50;
-- School district containment query
SELECT p.id, p.list_price, sd.name AS district_name
FROM properties p
JOIN school_districts sd ON ST_Contains(sd.boundary, p.geo_point)
WHERE p.status = 'Active'
AND sd.name = 'Scarsdale Union Free School District';
Quadtree Indexing for Map Tiles
When rendering properties on a map, the client needs to fetch only the properties visible in the current viewport. A quadtree spatial index divides the world into hierarchical tiles, allowing efficient retrieval of properties by map tile coordinates.
C#
public class QuadTreeIndex
{
private readonly Dictionary<string, List<PropertyGeoPoint>> _tiles = new();
public string GetTileKey(double lat, double lng, int zoom)
{
int scale = 1 << zoom;
int x = (int)Math.Floor((lng + 180.0) / 360.0 * scale);
int y = (int)Math.Floor((1.0 - Math.Log(Math.Tan(lat * Math.PI / 180.0) +
1.0 / Math.Cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * scale);
return $"{zoom}/{x}/{y}";
}
public List<PropertyGeoPoint> GetPropertiesInViewport(
double latMin, double lngMin, double latMax, double lngMax, int zoom)
{
var results = new List<PropertyGeoPoint>();
var tiles = GetTileRange(latMin, lngMin, latMax, lngMax, zoom);
foreach (var tile in tiles)
{
if (_tiles.TryGetValue(tile, out var properties))
results.AddRange(properties);
}
return results.DistinctBy(p => p.PropertyId).ToList();
}
}
Commute Time Search
One of the most valued search features is "find properties within X minutes commute of my workplace." This requires integrating with routing APIs (Google Maps Directions, Mapbox Routing) to compute isochrone polygons — the area reachable within a given time from a starting point.
C#
public async Task<List<PropertyListItem>> GetPropertiesWithinCommuteTime(
double workLat, double workLng, int maxMinutes, CommuteMode mode)
{
var isochrone = await _routingApi.GetIsochroneAsync(
workLat, workLng, maxMinutes, mode);
var wkt = $"POLYGON(({string.Join(",",
isochrone.Coordinates.Select(c => $"{c.Lng} {c.Lat}"))}))";
return await _context.Properties
.FromSqlRaw($@"
SELECT p.* FROM properties p
WHERE ST_Contains(
ST_GeomFromText('{wkt}', 4326),
p.geo_point
) AND p.status = 'Active'
ORDER BY p.list_price ASC
LIMIT 200")
.ToListAsync();
}
9. Property Detail Pages
The property detail page is the highest-value page on the platform — this is where buyers spend the most time and where lead generation happens. It must load fast (sub-500ms), display rich media, provide comprehensive data, and drive user action (contact agent, schedule showing, save property).
Detail Page Components
| Component | Data Source | Cache Strategy |
|---|---|---|
| Hero gallery (20+ photos) | S3 via CDN | CDN cache: 30 days; pre-load first 5 images |
| Virtual tour embed | Matterport API iframe | Lazy load on scroll; defer until user interaction |
| Price & key stats | PostgreSQL (source of truth) | Redis cache: 5 minutes; invalidate on price change |
| Description & features | PostgreSQL | Redis cache: 1 hour |
| Property history chart | Price history table | Redis cache: 24 hours (historical data changes rarely) |
| Tax assessment records | County assessor API / scraped data | Redis cache: 7 days |
| Comparable sales | ML service (Zestimate backend) | Redis cache: 1 hour |
| Neighborhood insights | Neighborhood service | Redis cache: 24 hours |
| School ratings | GreatSchools API | Redis cache: 7 days |
| Mortgage calculator | Computed client-side from rate API | Rate fetched once on page load |
| Agent card & contact form | Agent service | Redis cache: 1 hour |
Detail Page Rendering Strategy
C#
[ApiController]
[Route("api/v1/properties")]
public class PropertyDetailController : ControllerBase
{
[HttpGet("{id}")]
public async Task<ActionResult<PropertyDetailResponse>> GetPropertyDetail(Guid id)
{
var property = await _propertyService.GetWithCacheAsync(id);
var (history, taxes, comps, neighborhood, agent) = await Task.WhenAll(
_historyService.GetPriceHistoryAsync(id),
_taxService.GetTaxRecordsAsync(id),
_mlService.GetComparablesAsync(id, 5),
_neighborhoodService.GetAsync(property.NeighborhoodId),
_agentService.GetAsync(property.ListingAgentId)
);
return Ok(new PropertyDetailResponse
{
Property = property,
PriceHistory = history,
TaxRecords = taxes,
Comparables = comps,
Neighborhood = neighborhood,
Agent = agent,
MortgageEstimate = CalculateMortgage(property.ListPrice)
});
}
}
Property History & Price Chart
The price history component shows every recorded event for a property: listing date, price changes, listing status changes, and previous sale transactions. This data comes from MLS records, county recorder offices, and tax assessor databases. The chart renders as an interactive timeline showing price points, with annotations for major events (renovation, foreclosure, market crash).
10. ML-Based Price Estimation (Zestimate)
The automated home valuation model (similar to Zillow's Zestimate) is one of the most technically challenging and commercially valuable features of the platform. It must provide accurate property valuations using publicly available data, comparable sales, property characteristics, market trends, and location factors. Zillow's original Zestimate achieved a median error of ~7% for on-market homes — a benchmark that any serious platform must approach.
Feature Engineering
| Feature Category | Features | Importance |
|---|---|---|
| Property Characteristics | Bedrooms, bathrooms, sqft, lot size, year built, garage, pool, renovation status | High — direct value drivers |
| Location | Lat/lng, neighborhood, school district, walkability, crime rate, proximity to amenities | High — location is the #1 real estate factor |
| Comparable Sales | Nearby sales in last 6 months, adjusted for size/condition/age differences | Critical — the foundation of appraisal |
| Market Trends | Median price trend, inventory levels, days on market, price per sqft trend | Medium — captures market momentum |
| Tax Assessment | Last assessed value, assessment ratio, tax rate | Medium — baseline valuation signal |
| Listing Activity | Days on market, price reductions, DOM trend in area | Medium — signal of demand/supply |
| External Data | Interest rates, unemployment rate, GDP growth, building permits | Low-Medium — macroeconomic context |
Model Architecture
C#
public class ZestimateService
{
private readonly ISageMakerClient _sagemaker;
private readonly IFeatureStore _featureStore;
private readonly IComparableSalesEngine _compsEngine;
public async Task<ZestimateResult> EstimatePriceAsync(Guid propertyId)
{
var property = await _propertyRepository.GetByIdAsync(propertyId);
var features = new Dictionary<string, object>
{
["bedrooms"] = property.Bedrooms,
["bathrooms"] = property.Bathrooms,
["sqft"] = property.SquareFeet,
["lot_size"] = property.LotSizeSqFt ?? 0,
["year_built"] = property.YearBuilt,
["latitude"] = property.Latitude,
["longitude"] = property.Longitude,
["walk_score"] = await _featureStore.GetWalkScoreAsync(property.ZipCode),
["crime_index"] = await _featureStore.GetCrimeIndexAsync(property.NeighborhoodId),
["school_rating"] = await _featureStore.GetSchoolRatingAsync(property.SchoolDistrictId),
["distance_downtown"] = await _featureStore.GetDistanceToCBDAsync(property.Latitude, property.Longitude),
["num_comps_6m"] = 0,
["median_comp_price"] = 0.0,
["median_comp_price_sqft"] = 0.0,
["comp_size_adjusted_price"] = 0.0,
};
var comps = await _compsEngine.GetComparablesAsync(
property.Latitude, property.Longitude,
property.SquareFeet, 0.2, 6);
if (comps.Any())
{
features["num_comps_6m"] = comps.Count;
features["median_comp_price"] = comps.Median(c => c.SalePrice);
features["median_comp_price_sqft"] = comps.Median(c => c.SalePrice / c.SquareFeet);
features["comp_size_adjusted_price"] = comps.Average(c =>
c.SalePrice * ((double)property.SquareFeet / c.SquareFeet));
}
var prediction = await _sagemaker.InvokeEndpointAsync("zestimate-v2", features);
var uncertaintyFactors = CalculateUncertainty(property, comps);
return new ZestimateResult
{
EstimatedValue = prediction.MedianPrice,
ConfidenceLow = prediction.MedianPrice * (1 - uncertaintyFactors.UpperBound),
ConfidenceHigh = prediction.MedianPrice * (1 + uncertaintyFactors.UpperBound),
MedianError = prediction.MedianAbsolutePercentageError,
ComparableSalesUsed = comps.Count,
LastUpdated = DateTime.UtcNow,
ModelVersion = "v2.3.1"
};
}
}
Model Training & Evaluation
The model is trained on historical sale data with a time-based split to prevent data leakage. Features are computed using a "point-in-time" approach — for each historical sale, features are constructed only from data available before the sale date.
Python
import xgboost as xgb
from sklearn.metrics import mean_absolute_percentage_error
import numpy as np
params = {
'objective': 'reg:squarederror',
'max_depth': 8,
'learning_rate': 0.05,
'subsample': 0.8,
'colsample_bytree': 0.8,
'min_child_weight': 5,
'reg_alpha': 0.1,
'reg_lambda': 1.0,
'eval_metric': 'mae'
}
model = xgb.XGBRegressor(**params, n_estimators=2000)
model.fit(X_train, y_train, eval_set=[(X_val, y_val)],
early_stopping_rounds=100, verbose=False)
predictions = model.predict(X_test)
mape = mean_absolute_percentage_error(y_test, predictions)
print(f"Median APE: {np.median(np.abs((y_test - predictions) / y_test)) * 100:.1f}%")
print(f"Mean APE: {mape * 100:.1f}%")
print(f"Within 5%: {(np.abs((y_test - predictions) / y_test) < 0.05).mean() * 100:.1f}%")
print(f"Within 10%: {(np.abs((y_test - predictions) / y_test) < 0.10).mean() * 100:.1f}%")
11. Agent Profiles, Ratings & Lead Generation
Real estate agents are the primary revenue generators for the platform — they pay for premium placements, lead access, and marketing tools. The agent subsystem must support profile management, license verification, performance tracking, review/rating aggregation, and intelligent lead routing based on expertise, location, and availability.
Agent Profile Components
- License Verification: Automated validation against state licensing boards via API or web scraping. Display license status, issue date, brokerage affiliation, and any disciplinary actions.
- Transaction History: Past 12 months of closed transactions (sourced from MLS), showing volume, average sale price, days on market, and list-to-sale price ratio.
- Reviews & Ratings: Verified reviews from past clients. Weighted average with Bayesian smoothing to prevent gaming by agents with few reviews.
- Service Areas: Zip codes and neighborhoods where the agent operates, with "dominance score" based on transaction volume in each area.
- Specializations: Tags like "First-Time Buyers," "Luxury Homes," "Investment Properties," "Relocation," "Senior Housing."
Lead Scoring Model
C#
public class LeadScoringService
{
public decimal CalculateLeadScore(Inquiry inquiry, Property property, Agent agent)
{
decimal score = 0;
if (inquiry.Type == InquiryType.ShowingRequest) score += 40;
if (inquiry.Type == InquiryType.Offer) score += 80;
if (inquiry.Type == InquiryType.Question) score += 15;
if (inquiry.User.HasMortgagePreApproval) score += 20;
if (inquiry.User.PreApprovalAmount >= property.ListPrice) score += 10;
if (inquiry.User.HasViewedMultipleListings) score += 5;
if (inquiry.User.HasSavedSearches) score += 5;
if (inquiry.User.PreviousContactHistory > 3) score += 10;
if (inquiry.User.MovingTimeline == Timeline.WithinMonth) score += 15;
if (inquiry.User.MovingTimeline == Timeline.Within3Months) score += 10;
if (agent.ServiceAreas.Contains(property.ZipCode)) score += 10;
if (agent.Specializations.Contains(property.Type.ToString())) score += 5;
var ageHours = (DateTime.UtcNow - inquiry.CreatedAt).TotalHours;
if (ageHours > 48) score *= 0.7m;
if (ageHours > 168) score *= 0.3m;
return Math.Min(100, score);
}
}
Lead Routing
12. Mortgage Calculator & Financial Tools
The mortgage calculator is a critical conversion tool — it helps buyers understand affordability and drives pre-approval applications (a major revenue stream for the platform through lender partnerships). The calculator must handle multiple loan types, PMI calculations, property taxes, insurance, HOA fees, and provide amortization schedules.
Calculation Engine
C#
public class MortgageCalculator
{
public MortgageCalculationResult Calculate(MortgageRequest request)
{
double monthlyRate = request.AnnualInterestRate / 100.0 / 12.0;
int totalPayments = request.LoanTermYears * 12;
double principal = request.HomePrice * (1.0 - request.DownPaymentPercent / 100.0);
double monthlyPI;
if (monthlyRate == 0)
{
monthlyPI = principal / totalPayments;
}
else
{
monthlyPI = principal *
(monthlyRate * Math.Pow(1 + monthlyRate, totalPayments)) /
(Math.Pow(1 + monthlyRate, totalPayments) - 1);
}
double monthlyTax = (request.HomePrice * request.PropertyTaxRate / 100.0) / 12.0;
double monthlyInsurance = (request.HomePrice * 0.0035) / 12.0;
double monthlyPMI = 0;
if (request.DownPaymentPercent < 20)
{
monthlyPMI = principal * 0.005 / 12.0;
}
double monthlyHOA = request.HoaFeeMonthly;
double totalMonthlyPayment = monthlyPI + monthlyTax + monthlyInsurance + monthlyPMI + monthlyHOA;
var schedule = GenerateAmortizationSchedule(principal, monthlyRate, totalPayments, monthlyPI);
double maxAffordable = request.GrossMonthlyIncome * 0.28;
double maxWithDebt = (request.GrossMonthlyIncome * 0.36) - request.MonthlyDebtPayments;
return new MortgageCalculationResult
{
MonthlyPayment = totalMonthlyPayment,
MonthlyPrincipalInterest = monthlyPI,
MonthlyTax = monthlyTax,
MonthlyInsurance = monthlyInsurance,
MonthlyPMI = monthlyPMI,
MonthlyHOA = monthlyHOA,
TotalInterestPaid = (monthlyPI * totalPayments) - principal,
TotalCost = (totalMonthlyPayment * totalPayments) + (request.DownPaymentPercent / 100.0 * request.HomePrice),
AmortizationSchedule = schedule,
Affordability = new AffordabilityResult
{
MaxHomePriceFrontEnd = maxAffordable,
MaxHomePriceBackEnd = maxWithDebt > 0 ? maxWithDebt : 0,
DebtToIncomeRatio = (totalMonthlyPayment / request.GrossMonthlyIncome) * 100
}
};
}
private List<AmortizationEntry> GenerateAmortizationSchedule(
double principal, double monthlyRate, int totalPayments, double monthlyPI)
{
var schedule = new List<AmortizationEntry>();
double balance = principal;
for (int month = 1; month <= totalPayments; month++)
{
double interestPayment = balance * monthlyRate;
double principalPayment = monthlyPI - interestPayment;
balance -= principalPayment;
schedule.Add(new AmortizationEntry
{
Month = month,
Payment = monthlyPI,
Principal = principalPayment,
Interest = interestPayment,
Balance = Math.Max(0, balance)
});
}
return schedule;
}
}
Mortgage Calculator Display
| Component | Description | Interactive |
|---|---|---|
| Home Price Slider | $50K - $5M with step increments | Yes — real-time recalculation |
| Down Payment (% or $) | Toggle between percentage and dollar amount | Yes |
| Interest Rate | Fetched from lender API; adjustable | Yes |
| Loan Term | 15, 20, 30 year options | Yes |
| Payment Breakdown Chart | Donut chart: P&I, Tax, Insurance, PMI, HOA | Yes |
| Amortization Table | Expandable year-by-year breakdown | Yes — scroll, toggle |
| Affordability Calculator | Based on income and existing debts | Yes |
| Refinance Calculator | Compare current vs. new loan terms | Yes |
13. Saved Searches & Alerts
Saved searches are the primary engagement mechanism that keeps users returning to the platform. When a user saves a search with filters (e.g., "3+ bed, under $500K, in School District 5"), they expect to receive alerts whenever new matching listings appear or existing listings have significant price changes. The alert system must balance timeliness with notification fatigue.
Alert Delivery Architecture
C#
public class SavedSearchMatcherService
{
public async Task<List<AlertMatch>> FindMatchesAsync(PropertyChangeEvent changeEvent)
{
var potentialMatches = await _db.SavedSearches
.Where(s => s.IsAlertEnabled
&& s.UserId != changeEvent.ListingAgentId
&& MatchesCriteria(changeEvent.Property, s.SearchCriteria))
.ToListAsync();
var results = new List<AlertMatch>();
foreach (var savedSearch in potentialMatches)
{
var alertType = DetermineAlertType(changeEvent, savedSearch);
if (alertType == AlertType.None) continue;
results.Add(new AlertMatch
{
SavedSearchId = savedSearch.Id,
UserId = savedSearch.UserId,
PropertyId = changeEvent.Property.Id,
AlertType = alertType,
Message = FormatAlertMessage(changeEvent, savedSearch, alertType)
});
savedSearch.LastCheckedAt = DateTime.UtcNow;
}
await _db.SaveChangesAsync();
return results;
}
private AlertType DetermineAlertType(PropertyChangeEvent changeEvent, SavedSearch search)
{
if (changeEvent.ChangeType == ChangeType.NewListing)
return AlertType.NewListing;
if (changeEvent.ChangeType == ChangeType.PriceReduced)
{
var reductionPercent = (changeEvent.OldPrice - changeEvent.NewPrice) / changeEvent.OldPrice * 100;
if (reductionPercent >= 5) return AlertType.SignificantPriceReduction;
if (reductionPercent >= 2) return AlertType.PriceReduction;
}
if (changeEvent.ChangeType == ChangeType.StatusChanged
&& changeEvent.NewStatus == ListingStatus.UnderContract)
return AlertType.UnderContract;
return AlertType.None;
}
}
14. Favorites, Watchlist & Showing Scheduling
Favorites (also called a watchlist or "My Homes") allow users to track properties they're interested in over time. Unlike saved searches which match on filters, favorites are direct property references that the user explicitly bookmarked. The system tracks price changes, status updates, and new comparable sales for favorited properties.
Favorites Data Model
C#
public class Favorite
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid PropertyId { get; set; }
public string Notes { get; set; }
public int Rank { get; set; }
public DateTime AddedAt { get; set; }
public DateTime? LastPriceCheckAt { get; set; }
public List<PriceChangeNotification> PriceAlerts { get; set; }
}
public class ShowingRequest
{
public Guid Id { get; set; }
public Guid PropertyId { get; set; }
public Guid BuyerUserId { get; set; }
public Guid AgentId { get; set; }
public ShowingStatus Status { get; set; }
public DateTime RequestedDateTime { get; set; }
public DateTime? ConfirmedDateTime { get; set; }
public ShowingType Type { get; set; }
public int Attendees { get; set; }
public string SpecialInstructions { get; set; }
public DateTime CreatedAt { get; set; }
}
public class OpenHouse
{
public Guid Id { get; set; }
public Guid PropertyId { get; set; }
public Guid ListingAgentId { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public bool IsVirtual { get; set; }
public string VirtualTourUrl { get; set; }
public int ExpectedVisitors { get; set; }
public int ActualVisitors { get; set; }
public List<OpenHouseRegistration> Registrations { get; set; }
}
public class OpenHouseRegistration
{
public Guid Id { get; set; }
public Guid OpenHouseId { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public bool IsPreApproved { get; set; }
public bool IsWorkingWithAgent { get; set; }
public string BuyerAgentName { get; set; }
public DateTime RegisteredAt { get; set; }
public DateTime? CheckedInAt { get; set; }
}
Showing Scheduling Workflow
- Buyer clicks "Schedule a Showing" on a property detail page.
- Available time slots are displayed (agent sets availability in their profile settings). The system checks the agent's calendar for conflicts and excludes times already booked.
- Buyer selects a preferred time, chooses between in-person or virtual, enters number of attendees, and adds special instructions.
- The request is sent to the agent with a 15-minute acknowledgment SLA. If the agent doesn't respond, the request is escalated to the brokerage's showing coordinator.
- Once confirmed, both parties receive calendar invites (ICS format), the buyer gets preparation instructions (parking, gate codes), and a 24-hour reminder is scheduled.
- After the showing, the agent can optionally request feedback from the buyer via a short survey, and the buyer can leave a review for the property.
15. Property History & Records
Comprehensive property history is one of the most trusted features on a real estate platform. Buyers rely on sold price history, tax assessment records, building permits, and ownership changes to make informed decisions. This data is sourced from county recorder offices, tax assessor databases, MLS records, and public permit registries.
Data Sources & Integration
| Source | Data Provided | Update Frequency | Reliability |
|---|---|---|---|
| MLS (Historical) | Past listing prices, days on market, listing agent, sold price | Daily sync | High |
| County Recorder | Deed transfers, mortgage recordings, lien filings | Weekly | High (official records) |
| Tax Assessor | Assessed value, tax amount, lot size, building characteristics | Annual | High but may be outdated |
| Building Permits | Renovation permits, new construction, demolition | Monthly | Medium (not all counties digitized) |
| Foreclosure Records | Notice of Default, Lis Pendens, auction results | Daily | Medium (varies by jurisdiction) |
| Title Company | Title chain, easements, encumbrances | On-demand | High |
History Data Model
C#
public class PriceHistory
{
public Guid Id { get; set; }
public Guid PropertyId { get; set; }
public DateTime EventDate { get; set; }
public PriceEventType EventType { get; set; }
public decimal? Price { get; set; }
public string Source { get; set; }
public string Description { get; set; }
public string ListingAgent { get; set; }
public string BuyerAgent { get; set; }
public int? DaysOnMarket { get; set; }
}
public enum PriceEventType
{
Listed, PriceIncreased, PriceReduced, Sold,
PendingSale, Foreclosure, TaxAssessment,
PermitIssued, OwnershipTransfer
}
16. Neighborhood Data & Insights
Neighborhood data transforms a property listing from a standalone unit into a contextualized living experience. Buyers don't just buy a house — they buy a location, a community, a school district, a commute. The platform must aggregate data from multiple sources and present it in an intuitive, comparable format.
Neighborhood Data Components
| Data Category | Specific Metrics | Source |
|---|---|---|
| Walkability & Transit | Walk Score, Transit Score, Bike Score (0-100) | Walk Score API (Redfin) |
| Schools | Rating (1-10), student-teacher ratio, reviews, boundaries | GreatSchools API, state DOE |
| Crime | Crime rate per 1000 residents, violent vs. property crime | FBI UCR, SpotCrime, local PD APIs |
| Demographics | Median income, age distribution, education levels | US Census Bureau / ACS |
| Market Health | Median price, price trends, inventory, days on market | MLS data aggregated |
| Amenities | Restaurants, grocery stores, parks, hospitals, gyms | Google Places API, Foursquare |
| Environmental | Air quality index, flood zone, noise levels, wildfire risk | EPA, FEMA, NoiseMap |
| HOA Data | Monthly fees, CC&Rs, reserve fund health | HOA management companies |
Neighborhood Score Engine
C#
public class NeighborhoodScoreService
{
public async Task<NeighborhoodScore> CalculateScoreAsync(string neighborhoodId)
{
var data = await _neighborhoodRepository.GetAsync(neighborhoodId);
var weights = new Dictionary<string, double>
{
["walkability"] = 0.15,
["schools"] = 0.20,
["safety"] = 0.20,
["commute"] = 0.15,
["affordability"] = 0.10,
["amenities"] = 0.10,
["market_health"] = 0.10
};
var scores = new Dictionary<string, double>
{
["walkability"] = NormalizeScore(data.WalkScore, 0, 100),
["schools"] = NormalizeScore(data.AverageSchoolRating, 1, 10),
["safety"] = 1.0 - NormalizeScore(data.CrimeRateIndex, 0, 100),
["commute"] = NormalizeScore(100 - data.AverageCommuteMinutes, 0, 100),
["affordability"] = NormalizeScore(100 - data.PricePerSqftPercentile, 0, 100),
["amenities"] = NormalizeScore(data.AmenityCount, 0, 200),
["market_health"] = NormalizeScore(data.PriceGrowthPercentile, 0, 100)
};
double compositeScore = weights.Sum(w => w.Value * scores[w.Key]) * 100;
return new NeighborhoodScore
{
CompositeScore = Math.Round(compositeScore, 1),
CategoryScores = scores.ToDictionary(
kvp => kvp.Key,
kvp => Math.Round(kvp.Value * 100, 1)),
Rank = await CalculateRankAsync(compositeScore),
TotalNeighborhoods = await _neighborhoodRepository.CountAsync()
};
}
}
17. Inquiry Management & Open House Management
The inquiry management system is the operational backbone for agents. Every contact form submission, showing request, question about a property, and open house registration flows through this system. It must support lead tracking, automated follow-ups, response time monitoring, and integration with CRM systems.
Inquiry Lifecycle
C#
public class InquiryManager
{
public async Task<InquiryResult> ProcessInquiryAsync(InquiryRequest request)
{
var inquiry = new Inquiry
{
Id = Guid.NewGuid(),
PropertyId = request.PropertyId,
FromUserId = request.UserId,
ToAgentId = request.AgentId,
Type = request.Type,
Message = request.Message,
Status = InquiryStatus.New,
ShowingDateTime = request.PreferredShowingTime,
CreatedAt = DateTime.UtcNow
};
var leadScore = await _leadScoringService.CalculateLeadScoreAsync(
inquiry, request.Property, request.Agent);
inquiry.LeadScore = leadScore;
await _db.Inquiries.AddAsync(inquiry);
await _db.SaveChangesAsync();
if (leadScore > 70)
{
await _notificationService.SendHotLeadAlertAsync(request.Agent, inquiry);
}
else
{
await _notificationService.SendInboxNotificationAsync(request.Agent, inquiry);
}
await _notificationService.SendAutoReplyAsync(
request.UserEmail, request.Property, request.Agent);
await _eventTracker.TrackAsync(new InquiryEvent
{
PropertyId = request.PropertyId,
AgentId = request.AgentId,
LeadScore = leadScore,
InquiryType = request.Type
});
return new InquiryResult { InquiryId = inquiry.Id, LeadScore = leadScore };
}
}
Open House Management
Open houses are high-value events for lead generation. The platform supports both in-person and virtual open houses with registration, check-in, and post-event follow-up workflows.
- Pre-event: Agent creates open house with date/time, selects virtual/in-person, and optionally limits capacity. The listing is flagged with an "Open House This Weekend" badge in search results.
- Registration: Visitors pre-register via the property page or event link, providing name, email, phone, pre-approval status, and buyer agent info. Registration data feeds directly into the lead pipeline.
- Check-in: Agent uses a tablet app to scan QR codes or check in registered visitors. Walk-ins are captured with a quick-entry form.
- Post-event: All attendees receive a thank-you email with the property details and a feedback survey. Non-registered visitors who checked in get a follow-up nurture sequence. Agent gets attendance analytics and lead reports.
18. Rental Applications & Tenant Screening
Rental listings require a distinct workflow from sales listings. The platform must support rental application submission, tenant screening (credit checks, background checks, employment verification), and lease management. This is a high-volume, recurring revenue opportunity through screening service fees and premium rental listings.
Rental Application Pipeline
C#
public class TenantScreeningService
{
public async Task<ScreeningResult> ScreenAsync(RentalApplication application)
{
var result = new ScreeningResult
{
ApplicationId = application.Id,
ScreenedAt = DateTime.UtcNow
};
result.CreditReport = await _creditService.GetReportAsync(
application.FullName, application.SsnLast4, application.DateOfBirth);
result.BackgroundCheck = await _backgroundCheckService.CheckAsync(
application.FullName, application.DateOfBirth);
result.IncomeVerified = await _verificationService.VerifyEmploymentAsync(
application.EmployerName, application.AnnualIncome);
result.ScreeningScore = CalculateScreeningScore(result);
if (result.ScreeningScore >= 700 && result.IncomeVerified
&& !result.BackgroundCheck.HasCriminalRecord
&& !result.BackgroundCheck.HasEvictionHistory)
{
result.Recommendation = ScreeningRecommendation.Approved;
}
else if (result.ScreeningScore >= 600)
{
result.Recommendation = ScreeningRecommendation.Conditional;
}
else
{
result.Recommendation = ScreeningRecommendation.Denied;
result.AdverseActionReasons = BuildAdverseActionReasons(result);
}
await _encryptionService.EncryptAndStoreAsync(result);
return result;
}
}
19. Document Upload & Lease Management
The document management system handles the paperwork-intensive aspect of real estate transactions. Documents include purchase agreements, disclosures (lead-based paint, mold, termite), inspection reports, appraisals, title documents, and rental leases. The system must support version control, digital signatures, access control (only parties to the transaction can view), and long-term archival.
Document Architecture
C#
public class DocumentService
{
private readonly IS3Client _s3;
private readonly IDynamoDBClient _dynamo;
private readonly IDocuSignClient _docusign;
public async Task<DocumentUploadResult> UploadDocumentAsync(
DocumentUploadRequest request)
{
var key = $"transactions/{request.TransactionId}/documents/{Guid.NewGuid()}_{request.FileName}";
var uploadUrl = await _s3.GetPresignedUrlAsync(key, TimeSpan.FromMinutes(15));
var metadata = new DocumentMetadata
{
Id = Guid.NewGuid(),
TransactionId = request.TransactionId,
PropertyId = request.PropertyId,
FileName = request.FileName,
DocumentType = request.DocumentType,
S3Key = key,
UploadedBy = request.UploadedByUserId,
UploadedAt = DateTime.UtcNow,
FileSizeBytes = request.FileSize,
MimeType = request.MimeType,
AccessControlList = await BuildAccessListAsync(request.TransactionId),
RequiresSignature = request.RequiresSignature,
SignatureStatus = request.RequiresSignature
? SignatureStatus.Pending
: SignatureStatus.NotRequired
};
await _dynamo.PutItemAsync("Documents", metadata);
return new DocumentUploadResult
{
UploadUrl = uploadUrl,
DocumentId = metadata.Id
};
}
public async Task<SignResult> RequestSignatureAsync(
Guid documentId, List<SignerInfo> signers)
{
var metadata = await _dynamo.GetItemAsync<DocumentMetadata>(
"Documents", documentId);
var fileBytes = await _s3.GetObjectAsync(metadata.S3Key);
var envelope = await _docusign.CreateEnvelopeAsync(new EnvelopeRequest
{
Documents = new[]
{
new Document
{
DocumentBase64 = Convert.ToBase64String(fileBytes),
Name = metadata.FileName
}
},
Recipients = signers.Select(s => new Recipient
{
Name = s.Name,
Email = s.Email,
RoutingOrder = s.RoutingOrder.ToString(),
Tabs = new Tabs
{
SignHereTabs = new[]
{
new SignHere
{
DocumentId = "1",
PageNumber = "1",
XPosition = "100",
YPosition = "100"
}
}
}
}).ToList(),
Status = "sent"
});
metadata.SignatureStatus = SignatureStatus.Sent;
metadata.DocusignEnvelopeId = envelope.EnvelopeId;
await _dynamo.PutItemAsync("Documents", metadata);
return new SignResult { EnvelopeId = envelope.EnvelopeId };
}
}
Document Types & Access Control
| Document Type | Access | Retention | Signature Required |
|---|---|---|---|
| Purchase Agreement | Buyer, Seller, Agents | 7 years | Yes (all parties) |
| Lead-Based Paint Disclosure | Buyer, Seller | 3 years post-sale | Yes |
| Inspection Report | Buyer, Buyer's Agent | 3 years | No |
| Appraisal Report | Buyer, Lender | 5 years | No |
| Title Commitment | Buyer, Seller, Lender, Title Co. | Permanent | No |
| Rental Lease Agreement | Tenant, Landlord | Lease term + 3 years | Yes (all parties) |
| Tenant Screening Report | Landlord only | 2 years (FCRA) | No |
| HOA CC&Rs | Buyer, Listing Agent | Permanent | No |
20. Comparable Market Analysis & Market Trends
Comparable Market Analysis (CMA) is the primary tool agents use to price listings and advise sellers. The platform automates CMA generation by identifying comparable properties, adjusting for differences, and presenting a professional report. Market trends dashboards provide aggregate insights for neighborhoods, cities, and regions.
CMA Report Generation
C#
public class CmaReportService
{
public async Task<CmaReport> GenerateCmaAsync(CmaRequest request)
{
var comparables = await FindComparablesAsync(
request.SubjectProperty.Latitude,
request.SubjectProperty.Longitude,
request.SubjectProperty.SquareFeet,
request.SubjectProperty.Bedrooms,
request.SubjectProperty.PropertyType,
radiusMiles: 1.0,
soldWithinMonths: 6,
maxResults: 10);
var adjustments = comparables.Select(comp => new CmaAdjustment
{
ComparableId = comp.Id,
ComparablePrice = comp.SalePrice,
SizeAdjustment = CalculateSizeAdjustment(
comp.SquareFeet, request.SubjectProperty.SquareFeet),
BedroomAdjustment = CalculateBedroomAdjustment(
comp.Bedrooms, request.SubjectProperty.Bedrooms),
AgeAdjustment = CalculateAgeAdjustment(
comp.YearBuilt, request.SubjectProperty.YearBuilt),
ConditionAdjustment = EstimateConditionAdjustment(
comp, request.SubjectProperty),
LocationAdjustment = CalculateLocationAdjustment(
comp.LocationScore, request.SubjectLocationScore)
}).ToList();
foreach (var adj in adjustments)
{
adj.AdjustedPrice = adj.ComparablePrice
+ adj.SizeAdjustment
+ adj.BedroomAdjustment
+ adj.AgeAdjustment
+ adj.ConditionAdjustment
+ adj.LocationAdjustment;
}
var adjustedPrices = adjustments
.Select(a => a.AdjustedPrice).OrderBy(p => p).ToList();
var medianAdjusted = adjustedPrices.Median();
var stdDev = adjustedPrices.StandardDeviation();
return new CmaReport
{
SubjectProperty = request.SubjectProperty,
Comparables = adjustments,
RecommendedPriceRange = new PriceRange
{
Low = medianAdjusted - stdDev,
Median = medianAdjusted,
High = medianAdjusted + stdDev
},
AverageDaysOnMarket = comparables.Average(c => c.DaysOnMarket),
AveragePricePerSqft = comparables.Average(c => c.PricePerSqft),
MarketTrend = await GetMarketTrendAsync(request.SubjectProperty.ZipCode),
GeneratedAt = DateTime.UtcNow
};
}
}
Market Trends Dashboard
The market trends dashboard provides aggregate analytics at the neighborhood, city, zip code, and county levels. Key visualizations include:
- Median Price Trend: Line chart showing 12-month rolling median price with month-over-month and year-over-year comparisons.
- Inventory Levels: Active listings count over time, indicating supply pressure.
- Days on Market: Average and median DOM, signaling how fast homes sell.
- Price per Square Foot: Normalized metric for comparing values across property types.
- Absorption Rate: Months of inventory remaining (absorption rate = active listings / monthly sales).
- List-to-Sale Ratio: Average discount (or premium) from list price, indicating negotiation dynamics.
- New Listings vs. Closings: Pipeline health indicator — if new listings consistently outpace closings, inventory is building.
- Seasonality Patterns: Historical month-by-month patterns to predict upcoming market shifts.
21. API Design
The API follows RESTful conventions with JSON payloads, consistent error handling, and comprehensive pagination. All endpoints require authentication (JWT) except for public listing reads. Rate limiting is enforced per API key: 1000 requests/minute for authenticated users, 100/minute for anonymous.
Core API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/properties/search | Search listings with filters, geo, text | Optional |
| GET | /api/v1/properties/{id} | Property detail page data | Optional |
| POST | /api/v1/properties | Create new listing | Agent |
| PUT | /api/v1/properties/{id} | Update listing | Owner Agent |
| DELETE | /api/v1/properties/{id} | Deactivate listing | Owner Agent |
| POST | /api/v1/properties/{id}/photos | Upload photo (returns presigned URL) | Owner Agent |
| GET | /api/v1/properties/{id}/history | Price & status history | Optional |
| GET | /api/v1/properties/{id}/comparables | Comparable sales | Optional |
| GET | /api/v1/properties/{id}/neighborhood | Neighborhood data | Optional |
| POST | /api/v1/inquiries | Submit contact form / question | User |
| POST | /api/v1/showings | Request a showing | User |
| GET | /api/v1/users/favorites | List user's favorited properties | User |
| POST | /api/v1/users/favorites/{propertyId} | Add to favorites | User |
| DELETE | /api/v1/users/favorites/{propertyId} | Remove from favorites | User |
| GET | /api/v1/users/saved-searches | List saved searches | User |
| POST | /api/v1/users/saved-searches | Create saved search | User |
| POST | /api/v1/mortgage/calculate | Mortgage payment calculation | None |
| GET | /api/v1/agents/{id} | Agent profile | Optional |
| POST | /api/v1/rentals/applications | Submit rental application | User |
| POST | /api/v1/documents/upload | Get presigned upload URL | User/Agent |
| GET | /api/v1/market/trends/{geoId} | Market trend data | Optional |
| POST | /api/v1/cma/generate | Generate CMA report | Agent |
Search Request & Response
JSON
// Request: GET /api/v1/properties/search
{
"query": "modern kitchen",
"latitude": 40.7306,
"longitude": -73.9352,
"radiusMiles": 5,
"minPrice": 300000,
"maxPrice": 800000,
"minBedrooms": 2,
"minBathrooms": 1,
"propertyTypes": ["SingleFamily", "Condo"],
"features": ["Parking", "WasherDryer"],
"sortBy": "PriceAsc",
"page": 0,
"pageSize": 20
}
// Response
{
"results": [
{
"id": "a1b2c3d4",
"address": "123 Main St, Brooklyn, NY 11201",
"price": 549000,
"bedrooms": 3,
"bathrooms": 2,
"squareFeet": 1400,
"propertyType": "SingleFamily",
"yearBuilt": 1925,
"listingDate": "2026-06-15",
"daysOnMarket": 27,
"primaryPhoto": "https://cdn.example.com/properties/a1b2/thumb.webp",
"location": { "lat": 40.7312, "lng": -73.9345 },
"estimatedValue": 565000
}
],
"totalResults": 347,
"page": 0,
"pageSize": 20,
"aggregations": {
"propertyTypes": [
{ "key": "SingleFamily", "count": 189 },
{ "key": "Condo", "count": 98 }
]
}
}
/api/v1/) for breaking changes and header-based versioning for minor additions. Maintain backward compatibility for at least 2 major versions. Document all endpoints with OpenAPI 3.0 (Swagger) and provide SDKs for JavaScript, Python, and C#.
22. Monitoring, Security & Compliance
Monitoring & Observability
A real estate platform requires comprehensive monitoring to maintain the trust of buyers, sellers, and agents. Downtime during peak buying season (March-June) directly translates to lost revenue and user churn.
| Metric Category | Key Metrics | Alert Threshold |
|---|---|---|
| Search Performance | Latency (p50, p95, p99), throughput, error rate | p99 > 200ms, error rate > 1% |
| Listing Freshness | MLS sync lag, update propagation time | Sync lag > 15 minutes |
| Media Pipeline | Upload success rate, processing time, CDN hit ratio | CDN hit ratio < 90% |
| Lead Pipeline | Lead volume, response time, conversion rate | Avg response > 30 min |
| ML Model | Prediction accuracy, latency, feature freshness | MdAPE > 8% |
| Infrastructure | CPU, memory, disk, network per service | CPU > 80% sustained |
| Business | DAU, listings viewed, leads submitted, showings | Day-over-day decline > 15% |
Security Architecture
- Authentication: OAuth2/OpenID Connect with social login (Google, Facebook, Apple). Multi-factor authentication for agents and users with financial operations. Session management via short-lived JWTs (15 min) with refresh token rotation.
- Authorization: Role-based access control (RBAC) with four roles: Buyer, Seller, Agent, Admin. Agents can only manage their own listings. Users can only view their own saved searches, favorites, and rental applications.
- Data Protection: All PII (Social Security numbers for tenant screening, financial data) encrypted at rest using AES-256 with AWS KMS-managed keys. Sensitive documents encrypted with per-document keys. TLS 1.3 for all data in transit.
- Input Validation: Server-side validation on all inputs. Listing descriptions scanned for XSS, SQL injection, and Fair Housing violations. Photo uploads scanned for malware via ClamAV.
- Rate Limiting: Per-user and per-IP rate limiting to prevent scraping and abuse. Aggressive rate limits on authentication endpoints (5 failed attempts = 15-minute lockout). CAPTCHA on listing creation and inquiry submission.
- Audit Logging: All data access and mutations logged with user identity, timestamp, IP, and action. 90-day retention for compliance. Immutable audit log stored in append-only S3 bucket.
Fair Housing Compliance
- Search algorithms do not discriminate by steering users toward or away from neighborhoods based on protected characteristics.
- Listing descriptions are automatically scanned for discriminatory language.
- Equal housing opportunity notices are displayed on all listing pages.
- Agent profiles and reviews are monitored for discriminatory content.
- ML models are audited for disparate impact.
- ADA compliance: all web pages meet WCAG 2.1 AA standards.
ADA Compliance Checklist
| Requirement | Implementation | Status |
|---|---|---|
| Keyboard Navigation | All interactive elements focusable and operable via keyboard | Must Have |
| Screen Reader Support | ARIA labels on all images, form fields, and interactive elements | Must Have |
| Color Contrast | Minimum 4.5:1 contrast ratio for text, 3:1 for UI components | Must Have |
| Alt Text | Every listing photo has descriptive alt text | Must Have |
| Virtual Tour Accessibility | Audio descriptions for 3D tours, text alternatives for floor plans | Should Have |
| Captions | Video walkthroughs include closed captions | Must Have |
| Error Handling | Form errors announced to screen readers | Must Have |
23. Testing Strategy
A robust testing strategy for a real estate platform must cover functional correctness, geospatial accuracy, search relevance, ML model quality, and compliance verification. The multi-service architecture demands both unit-level and integration-level testing.
Testing Pyramid
| Test Type | Scope | Count Target | Execution Time |
|---|---|---|---|
| Unit Tests | Business logic, calculations, data transformations | 2,000+ | < 5 minutes |
| Integration Tests | Service-to-DB, service-to-service, API contract tests | 500+ | < 15 minutes |
| Contract Tests | API request/response schemas between frontend and backend | 200+ | < 5 minutes |
| End-to-End Tests | Critical user journeys (search, view, contact, schedule) | 50+ | < 30 minutes |
| Performance Tests | Load testing, stress testing for search and listing pages | 20+ scenarios | < 60 minutes |
| ML Model Tests | Prediction accuracy, feature drift, A/B test evaluation | Continuous | Nightly pipeline |
Key Test Scenarios
C#
[TestClass]
public class PropertySearchTests
{
[TestMethod]
public async Task Search_ByRadius_ReturnsOnlyPropertiesWithinDistance()
{
await IndexPropertyAsync("Prop1", lat: 40.7306, lng: -73.9352);
await IndexPropertyAsync("Prop2", lat: 40.7580, lng: -73.9855);
await IndexPropertyAsync("Prop3", lat: 40.0583, lng: -74.4056);
var results = await _searchService.SearchAsync(new PropertySearchRequest
{
Latitude = 40.7306,
Longitude = -73.9352,
RadiusMiles = 5
});
Assert.AreEqual(2, results.TotalResults);
}
[TestMethod]
public async Task Search_DrawOnMap_ReturnsOnlyPropertiesInPolygon()
{
await IndexPropertiesInTestAreaAsync();
var polygon = new List<(double Lat, double Lng)>
{
(40.7300, -73.9360), (40.7300, -73.9340),
(40.7310, -73.9340), (40.7310, -73.9360)
};
var results = await _searchService.SearchInPolygonAsync(polygon);
Assert.IsTrue(results.Items.All(p =>
IsPointInPolygon(p.Latitude, p.Longitude, polygon)));
}
}
[TestClass]
public class MortgageCalculatorTests
{
[TestMethod]
public void Calculate_StandardLoan_ReturnsCorrectPayment()
{
var calculator = new MortgageCalculator();
var result = calculator.Calculate(new MortgageRequest
{
HomePrice = 500000,
DownPaymentPercent = 20,
AnnualInterestRate = 6.5,
LoanTermYears = 30,
PropertyTaxRate = 1.2,
HoaFeeMonthly = 0
});
Assert.AreEqual(2528.27, result.MonthlyPrincipalInterest, 1.0);
Assert.AreEqual(500.0, result.MonthlyTax, 1.0);
}
[TestMethod]
public void Calculate_BelowTwentyPercentDown_IncludesPMI()
{
var result = new MortgageCalculator().Calculate(new MortgageRequest
{
HomePrice = 500000,
DownPaymentPercent = 10,
AnnualInterestRate = 6.5,
LoanTermYears = 30,
PropertyTaxRate = 1.2,
HoaFeeMonthly = 0
});
Assert.IsTrue(result.MonthlyPMI > 0);
}
}
[TestClass]
public class FairHousingComplianceTests
{
[TestMethod]
public void ListingDescription_DiscriminatoryLanguage_FlaggedForReview()
{
var scanner = new FairHousingScanner();
var violations = scanner.Scan(
"Beautiful home, perfect for a Christian family. " +
"No children allowed. Close to the synagogue. " +
"Ideal for young professionals.");
Assert.AreEqual(3, violations.Count);
Assert.IsTrue(violations.Any(v => v.Category == "FamilialStatus"));
Assert.IsTrue(violations.Any(v => v.Category == "Religion"));
}
}
24. Interview Q&A Deep Dive
Q1: How would you handle the MLS data synchronization problem? Listings arrive in different formats and update frequencies.
Answer: I would build a canonical data model that normalizes all MLS data into a unified schema. The ingestion pipeline uses an adapter pattern — each MLS feed has a dedicated adapter that translates from its specific format (RETS, IDX XML, DAML JSON) into our canonical model. Deduplication is handled by matching on address + property type + listing price within a time window, using a probabilistic matching algorithm (SimHash for address normalization) to account for address format variations. The pipeline uses Kafka for reliable message delivery with exactly-once semantics, and Debezium CDC for streaming changes from PostgreSQL to Elasticsearch. For conflict resolution (same property updated by two sources), we use a "last writer wins" strategy with source priority — MLS data takes precedence over agent manual edits, and county assessor data is authoritative for tax records.
Q2: How do you design the geospatial search to handle the "draw on map" feature efficiently?
Answer: The draw-on-map feature has a two-phase execution strategy. First, the client sends the polygon to the API, which computes a bounding box and uses Elasticsearch's geo_bounding_box filter as a fast pre-filter — this eliminates 90%+ of candidates using the inverted index. Then, only the candidate set (typically 50-200 properties) is checked against PostGIS's ST_Contains for exact polygon containment. This hybrid approach keeps latency under 100ms even for complex polygons. For caching, I'd cache common polygon shapes (school districts, neighborhood boundaries, zip codes) as pre-computed PostGIS queries with materialized views. The polygon itself can be simplified using the Douglas-Peucker algorithm to reduce coordinate count while maintaining accuracy within 10 meters.
Q3: How would you ensure the Zestimate doesn't develop bias against certain neighborhoods?
Answer: Fair lending compliance is paramount. I would implement a multi-layered fairness framework: (1) Feature auditing — remove or constrain features that serve as proxies for race or protected characteristics. (2) Disparate impact testing — regularly evaluate whether the model's error rates differ significantly across neighborhoods with different demographic compositions. (3) Fairness constraints — add fairness-aware regularization terms to the model's loss function. (4) Human-in-the-loop review — any property where the Zestimate differs from the agent's CMA by more than 15% triggers a manual review. (5) Regular bias audits — quarterly third-party audits using the Equal Credit Opportunity Act framework. (6) Explainability — provide feature importance for each prediction so auditors can understand why a price was predicted.
Q4: How do you handle the scale of photo storage and delivery for 110M+ properties?
Answer: Media storage and delivery is the largest cost driver. The architecture uses a multi-tier approach: (1) Original photos stored in S3 with Intelligent-Tiering lifecycle policies — photos move to cheaper storage tiers as they age. (2) Four processed sizes (thumbnail, medium, large, original) stored in a CloudFront-origin bucket with aggressive CDN caching (30-day TTL, 90%+ hit ratio expected since listing photos are essentially immutable). (3) Responsive images served via srcset with WebP/AVIF format negotiation — modern formats reduce file size by 30-50% vs. JPEG. (4) Lazy loading with blurHash placeholders for below-the-fold images. (5) Virtual tour embeds (Matterport) loaded only on user interaction. At Zillow's scale, they reportedly spend $100M+/year on media infrastructure alone.
Q5: How do you design the saved search alert system to be both timely and non-spammy?
Answer: The alert system uses a three-tier notification strategy with user-configurable frequency. For real-time alerts (new listings, price reductions > 5%), the matching runs on every MLS sync cycle (every 15 minutes) using a pre-indexed criteria match against Elasticsearch. For daily/weekly digests, a batch job collects all matching events and composes a single email. Anti-spam measures include: (1) Per-property suppression — no more than one notification per property per 24 hours. (2) Per-user throttling — maximum 3 push notifications per day for the same saved search. (3) Adaptive frequency — if a user hasn't opened their last 3 email digests, automatically downgrade from daily to weekly. (4) Snooze option — users can temporarily pause alerts for 1 week. (5) Smart ranking — in daily digests, properties are ranked by freshness, price changes, and estimated relevance based on browsing behavior.
Q6: How do you prevent agents from gaming the system with fake listings or manipulated photos?
Answer: Anti-gaming requires a multi-pronged approach: (1) MLS verification — all listings must have a valid MLS number that can be verified against the IDX feed. (2) Photo forensics — perceptual hashing (pHash) detects duplicate photos across listings. Reverse image search catches stock photos. (3) Price consistency — listings priced more than 50% below Zestimate are flagged for manual review. (4) Behavioral signals — agents who create many listings that quickly expire receive reduced search ranking. (5) Community reporting — users and other agents can flag suspicious listings. (6) License verification — automated checks against state licensing boards. (7) Audit trail — every listing change is logged with IP address and user agent.
Q7: How would you handle the document management system for real estate transactions?
Answer: The document system uses a layered architecture: S3 for binary blob storage with server-side encryption (SSE-KMS), DynamoDB for metadata and access control lists, and an e-signature integration (DocuSign/HelloSign) for contract workflows. Each transaction has a "document workspace" — a virtual folder with role-based access (buyer can see inspection reports, seller can see disclosures, both can see the purchase agreement). Documents are versioned using S3 versioning, and all access is logged for audit. For lease management, we track document state (draft → in review → signed → active → expired) with automated reminders for expiring leases. The key technical challenge is access control: ensuring that a buyer's agent can't see another buyer's financial documents, while still allowing the listing agent to view all transaction documents for their listing.
Q8: Design the system for comparable market analysis (CMA). How do you identify and adjust for comparable properties?
Answer: CMA is fundamentally a nearest-neighbor problem with domain-specific adjustments. The algorithm: (1) Find candidate comparables within 1 mile that sold in the last 6 months, with similar property type. (2) Rank by a composite similarity score weighing square footage difference (most important), bedroom count, year built proximity, and lot size. (3) Apply adjustment factors: size adjustment (price per sqft difference × sqft delta), bedroom adjustment ($5K-15K per bedroom difference), age adjustment ($500/year for properties > 30 years old), condition adjustment (requires manual input or ML inference from photos), and location adjustment (using neighborhood score difference). (4) Weight more recent sales and closer properties higher. (5) Apply a confidence interval based on the number and quality of comparables found — fewer comparables = wider confidence interval. The ML version of this can be trained on historical appraisal data to learn the optimal adjustment weights.
Q9: How do you handle real-time map updates when properties are listed, sold, or price-reduced?
Answer: The map view uses a combination of polling and Server-Sent Events (SSE) for real-time updates. The initial map load fetches properties in the viewport via the standard search API. For real-time updates: (1) WebSocket connection (or SSE for simpler implementation) subscribes to a geographic channel based on the current viewport. (2) When a property changes within the viewport, the change event is pushed to connected clients. (3) The client receives the change and updates the specific marker (new price, status badge, removed if sold). (4) For clustering at low zoom levels, cluster aggregates are updated periodically (every 30 seconds) rather than on every individual change, to prevent constant cluster recalculation. (5) Viewport changes trigger re-subscription to the appropriate geographic channel. This approach keeps the map responsive without hammering the server with constant polling.
Q10: Walk me through the complete user journey for a home buyer, from first search to closing.
Answer: The complete buyer journey: (1) Discovery: User searches for properties by location, filters, and keywords. Results display on a map with clustering. (2) Exploration: User views property details, browses photos, watches virtual tours, checks neighborhood data (schools, walkability, crime). Uses the mortgage calculator to assess affordability. (3) Shortlisting: User creates an account, saves favorite properties with personal notes, and sets up saved searches with alerts. (4) Engagement: User contacts an agent via the property page contact form, schedules showings for top picks, and attends open houses. (5) Evaluation: User reviews comparable sales, tax history, and price trends. Agent generates a CMA report. (6) Financial Prep: User uses the mortgage calculator extensively, gets pre-approved through a partner lender, and receives pre-approval documentation. (7) Offer: Agent drafts a purchase agreement, uploads it for e-signature. Buyer and seller counter-offer through the document workflow. (8) Due Diligence: Inspection report uploaded, appraisal ordered, title search completed. All documents managed in the transaction workspace. (9) Closing: Final documents signed, funds transferred, deed recorded. (10) Post-Closing: User invited to leave a review for the agent, property transitions to "Sold" status in the system, user receives homeowner resource emails.