system-design51 min read

How to Design a Real Estate Listing & Search Platform — A Senior+ Guide | Ayodhyya

How to Design a Real Estate Listing & Search Platform

Building Zillow/Rightmove at Scale — Geospatial Search, ML Price Estimation, Compliance & Full Lifecycle

Senior+ System Design Guide 10,000+ Words 24 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

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.

Key Insight: A real estate platform is not just a search engine with listings — it is a full lifecycle transaction support system. It must handle everything from initial property discovery through mortgage pre-approval, offer submission, document signing, and closing coordination. The engineering complexity rivals that of financial trading platforms, with the added dimension of geospatial intelligence and regulatory compliance.

Real-World Scale & Case Studies

PlatformScaleKey Technical Innovation
Zillow110M+ homes tracked, 2B+ page views/monthZestimate ML model, 3D home tours, instant offers
RightmoveUK's largest property portal, 90%+ market shareAdvanced map-based search, school catchment overlays
Redfin100K+ home tours/year, 30+ marketsAgent matching algorithm, 3D walkthroughs, real-time alerts
Realtor.com1B+ property data updates/monthFloor plan analysis, neighborhood scoring, off-market leads
CoStar/LoopNet6B+ sq ft of commercial property trackedCommercial-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

RequirementTargetRationale
Search latency (p99)< 200msUsers expect near-instant results when exploring neighborhoods
Detail page load (p95)< 500msHigh engagement page; slow loads lose buyers
Photo delivery (CDN)< 100ms (cache hit)Image-heavy pages with 20+ photos per listing
Availability99.95%Real estate is 24/7; downtime during weekends is catastrophic
Data freshness< 15 min for MLS syncStale listings erode trust
Concurrency100K concurrent usersPeak traffic during spring buying season, open house weekends
StoragePB-scale photos/videos110M homes × 50 photos avg × 5MB = 27.5 PB raw
ComplianceFair Housing Act, ADA, GDPR/CCPALegal 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)

ServiceConfigurationMonthly 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 Storage80TB + CDN~$2,000
CloudFront CDN10TB/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
Cost Scaling Note: Media storage and CDN delivery dominate costs at scale. Zillow spends an estimated $100M+/year on infrastructure, with media delivery being the single largest line item. Aggressive image optimization (WebP/AVIF, responsive sizing, lazy loading) and aggressive CDN caching are essential cost control measures.

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 TypeTechnologyRationale
Property recordsPostgreSQL + PostGISACID transactions, geospatial indexing, relational integrity for MLS data
Search indexElasticsearchFull-text search, faceted filtering, geo-bounding-box queries, relevance tuning
Photos/videosS3 + CloudFrontUnlimited storage, CDN delivery, lifecycle policies for cost optimization
Sessions/cacheRedis ClusterSub-ms latency for session storage, search result caching, rate limiting
User dataPostgreSQLRelational integrity, joins with property data
Analytics/eventsApache Kafka → RedshiftEvent streaming for click tracking, search analytics, ML feature engineering
Document storageS3 + DynamoDB metadataLarge binary blobs in S3, metadata/indexing in DynamoDB
Design Decision — Dual Write for Search: Property writes go to PostgreSQL as the source of truth, then a CDC (Change Data Capture) pipeline via Debezium streams changes to Elasticsearch within 200ms. This avoids dual-write consistency issues while keeping search indices nearly real-time.

5. High-Level Architecture Overview

graph TB subgraph Clients["Client Layer"] WEB["Web App
(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

ServiceResponsibilityKey Tech
Search ServiceQuery processing, filter application, geo-queries, result ranking, cachingElasticsearch, Redis, PostGIS
Listing ServiceCRUD operations, MLS sync, status lifecycle, validation, pricing updatesPostgreSQL, Debezium CDC
Media ServicePhoto upload, processing pipeline, CDN management, virtual tour embeddingS3, Lambda, CloudFront
User ServiceAuthentication, profiles, preferences, saved searches, favoritesPostgreSQL, JWT, OAuth2
Agent ServiceAgent profiles, license verification, reviews, performance metricsPostgreSQL, 3rd-party license APIs
Lead & Inquiry ServiceContact forms, lead scoring, showing requests, follow-up automationPostgreSQL, ML scoring model
ML Price EstimationZestimate generation, comparable sales analysis, market trend predictionsSageMaker, feature store
Neighborhood ServiceSchool data, crime stats, walkability scores, boundary polygonsPostGIS, 3rd-party APIs
Mortgage CalculatorPayment calculations, affordability analysis, lender integrationMath engine, rate APIs
Document ServiceContract uploads, disclosures, e-signature integration, lease managementS3, DynamoDB, DocuSign API
Rental ServiceRental applications, tenant screening, lease lifecyclePostgreSQL, screening APIs
Notification ServiceEmail alerts, push notifications, SMS for showing remindersSES, 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

graph LR A["MLS Data Feed
(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:

  1. 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.
  2. 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).
  3. 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).
  4. 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.
Performance Optimization: Lazy loading images below the fold reduces initial page weight by 70%. BlurHash placeholders provide instant visual feedback while images load. Progressive JPEG encoding ensures the first bytes of each image render a recognizable preview within 50ms.

Listing Validation Rules

FieldValidation RuleError Handling
AddressGeocoded to exact coordinates via Google Maps APIReject if geocoding fails; flag if confidence < 0.8
Price$10,000 - $100,000,000; must be within 30% of ZestimateWarn if outlier; require agent justification
Square Feet100 - 100,000; cross-validate with tax recordsFlag discrepancy > 20% for review
PhotosMinimum 1 exterior; no duplicates (perceptual hash)Reject duplicates; require minimum count
Bedrooms0 - 20; must match floor plan data if availableFlag inconsistency
Description50-5,000 characters; no discriminatory languageAI scan for Fair Housing violations; reject if flagged
Fair Housing Compliance: Listing descriptions must be automatically scanned for discriminatory language. Words indicating preference based on race, religion, national origin, sex, familial status, or disability violate the Fair Housing Act. The platform must filter phrases like "perfect for a Christian family," "no children," or "close to the synagogue" and prompt agents to rewrite compliant descriptions. This is not optional — HUD actively investigates violations with penalties up to $100,000+.

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();
}
Performance Consideration: Isochrone queries are expensive because they require calling external routing APIs. Cache isochrone results for 24 hours (commute routes don't change frequently) and pre-compute isochrones for the top 1,000 most-searched workplace locations. Store cached isochrones in a spatial lookup table indexed by origin + max_minutes + commute_mode.

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

ComponentData SourceCache Strategy
Hero gallery (20+ photos)S3 via CDNCDN cache: 30 days; pre-load first 5 images
Virtual tour embedMatterport API iframeLazy load on scroll; defer until user interaction
Price & key statsPostgreSQL (source of truth)Redis cache: 5 minutes; invalidate on price change
Description & featuresPostgreSQLRedis cache: 1 hour
Property history chartPrice history tableRedis cache: 24 hours (historical data changes rarely)
Tax assessment recordsCounty assessor API / scraped dataRedis cache: 7 days
Comparable salesML service (Zestimate backend)Redis cache: 1 hour
Neighborhood insightsNeighborhood serviceRedis cache: 24 hours
School ratingsGreatSchools APIRedis cache: 7 days
Mortgage calculatorComputed client-side from rate APIRate fetched once on page load
Agent card & contact formAgent serviceRedis 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).

SEO Optimization: Property detail pages are the primary organic traffic driver. Each page must include structured data (Schema.org RealEstateListing), proper Open Graph tags for social sharing, canonical URLs, and comprehensive meta descriptions. Server-side rendering (Next.js SSR/SSG) ensures search engines can crawl all listing content without JavaScript execution.

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 CategoryFeaturesImportance
Property CharacteristicsBedrooms, bathrooms, sqft, lot size, year built, garage, pool, renovation statusHigh — direct value drivers
LocationLat/lng, neighborhood, school district, walkability, crime rate, proximity to amenitiesHigh — location is the #1 real estate factor
Comparable SalesNearby sales in last 6 months, adjusted for size/condition/age differencesCritical — the foundation of appraisal
Market TrendsMedian price trend, inventory levels, days on market, price per sqft trendMedium — captures market momentum
Tax AssessmentLast assessed value, assessment ratio, tax rateMedium — baseline valuation signal
Listing ActivityDays on market, price reductions, DOM trend in areaMedium — signal of demand/supply
External DataInterest rates, unemployment rate, GDP growth, building permitsLow-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}%")
Model Performance Targets: Median Absolute Percentage Error (MdAPE) < 6% for on-market properties, < 8% for off-market. At least 70% of predictions should fall within 10% of the actual sale price. These metrics are comparable to professional appraisals and exceed the accuracy of comparative market analysis performed by individual agents.

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

graph TB A["New Lead Inquiry"] --> B["Lead Scoring Engine"] B --> C{"Score > 70?"} C -->|"Yes: Hot Lead"| D["Route to Top Matching Agent"] C -->|"No: Warm Lead"| E["Queue in Agent Inbox"] C -->|"Score < 30"| F["Automated Nurture Email"] D --> G["Agent Notification (Push + SMS)"] G --> H{"Agent responds in 15 min?"} H -->|"Yes"| I["Continue Conversation"] H -->|"No"| J["Re-route to Backup Agent"]
Lead Response Time: Research shows that responding to a lead within 5 minutes makes you 21x more likely to convert compared to a 30-minute response. The platform must enforce SLAs on agent response times, with automatic re-routing if the primary agent doesn't acknowledge the lead within 15 minutes during business hours.

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

ComponentDescriptionInteractive
Home Price Slider$50K - $5M with step incrementsYes — real-time recalculation
Down Payment (% or $)Toggle between percentage and dollar amountYes
Interest RateFetched from lender API; adjustableYes
Loan Term15, 20, 30 year optionsYes
Payment Breakdown ChartDonut chart: P&I, Tax, Insurance, PMI, HOAYes
Amortization TableExpandable year-by-year breakdownYes — scroll, toggle
Affordability CalculatorBased on income and existing debtsYes
Refinance CalculatorCompare current vs. new loan termsYes

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

graph TB A["New/Updated Listing"] --> B["Kafka Event"] B --> C["Saved Search Matcher Service"] C --> D{"Alert Frequency"} D -->|"Instant"| E["Real-time Push Notification"] D -->|"Daily"| F["Batch Job (6 AM Local)"] D -->|"Weekly"| G["Batch Job (Sunday 8 AM)"] E --> H["Push + Email + SMS"] F --> I["Email Digest"] G --> J["Email Digest"]
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;
    }
}
Notification Fatigue Prevention: Implement a suppression window: no more than one notification per property per 24 hours, and no more than 3 notifications per user per day for the same saved search. For daily/weekly digests, batch all matching properties into a single email with a preview grid. Include an "unsubscribe" link and a "snooze for 1 week" option in every notification.

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

  1. Buyer clicks "Schedule a Showing" on a property detail page.
  2. 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.
  3. Buyer selects a preferred time, chooses between in-person or virtual, enters number of attendees, and adds special instructions.
  4. 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.
  5. Once confirmed, both parties receive calendar invites (ICS format), the buyer gets preparation instructions (parking, gate codes), and a 24-hour reminder is scheduled.
  6. 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

SourceData ProvidedUpdate FrequencyReliability
MLS (Historical)Past listing prices, days on market, listing agent, sold priceDaily syncHigh
County RecorderDeed transfers, mortgage recordings, lien filingsWeeklyHigh (official records)
Tax AssessorAssessed value, tax amount, lot size, building characteristicsAnnualHigh but may be outdated
Building PermitsRenovation permits, new construction, demolitionMonthlyMedium (not all counties digitized)
Foreclosure RecordsNotice of Default, Lis Pendens, auction resultsDailyMedium (varies by jurisdiction)
Title CompanyTitle chain, easements, encumbrancesOn-demandHigh

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
}
Data Quality Challenge: Public records are notoriously inconsistent across counties. Address formats vary, lot sizes may be recorded in different units, and some counties still have partially digitized records. The platform must implement a robust address normalization pipeline (USPS CASS certification) and a confidence scoring system for each data point based on source reliability and cross-reference consistency.

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 CategorySpecific MetricsSource
Walkability & TransitWalk Score, Transit Score, Bike Score (0-100)Walk Score API (Redfin)
SchoolsRating (1-10), student-teacher ratio, reviews, boundariesGreatSchools API, state DOE
CrimeCrime rate per 1000 residents, violent vs. property crimeFBI UCR, SpotCrime, local PD APIs
DemographicsMedian income, age distribution, education levelsUS Census Bureau / ACS
Market HealthMedian price, price trends, inventory, days on marketMLS data aggregated
AmenitiesRestaurants, grocery stores, parks, hospitals, gymsGoogle Places API, Foursquare
EnvironmentalAir quality index, flood zone, noise levels, wildfire riskEPA, FEMA, NoiseMap
HOA DataMonthly fees, CC&Rs, reserve fund healthHOA 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()
        };
    }
}
Map Overlay Rendering: Neighborhood boundaries are rendered as GeoJSON polygons on the map view. At zoom level 12+, boundaries become visible and color-coded by composite score (green = high, yellow = medium, red = low). Users can toggle overlays for schools, crime hotspots, flood zones, and public transit routes. Each overlay uses vector tile rendering for smooth performance.

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

graph LR A["Tenant Submits Application"] --> B["Document Collection"] B --> C["Credit Check (Experian/TransUnion)"] C --> D["Background Check"] D --> E["Employment Verification"] E --> F["Landlord Reference Check"] F --> G{"Score > Threshold?"} G -->|"Approved"| H["Offer Lease Generation"] G -->|"Conditional"| I["Request Co-signer"] G -->|"Denied"| J["Adverse Action Notice (FCRA)"]
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;
    }
}
Legal Compliance: Tenant screening is governed by the Fair Credit Reporting Act (FCRA), which requires adverse action notices when denying applications based on credit or background check data. The platform must provide clear, written reasons for denial and inform applicants of their right to dispute the information. Screening data must be encrypted at rest and access-logged for audit trails.

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 TypeAccessRetentionSignature Required
Purchase AgreementBuyer, Seller, Agents7 yearsYes (all parties)
Lead-Based Paint DisclosureBuyer, Seller3 years post-saleYes
Inspection ReportBuyer, Buyer's Agent3 yearsNo
Appraisal ReportBuyer, Lender5 yearsNo
Title CommitmentBuyer, Seller, Lender, Title Co.PermanentNo
Rental Lease AgreementTenant, LandlordLease term + 3 yearsYes (all parties)
Tenant Screening ReportLandlord only2 years (FCRA)No
HOA CC&RsBuyer, Listing AgentPermanentNo

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.
Data Freshness: Market trends are computed as materialized views in the data warehouse, refreshed nightly from MLS data. For real-time indicators (active listing count, new listings today), a streaming pipeline updates Elasticsearch aggregations every 15 minutes.

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

MethodEndpointDescriptionAuth
GET/api/v1/properties/searchSearch listings with filters, geo, textOptional
GET/api/v1/properties/{id}Property detail page dataOptional
POST/api/v1/propertiesCreate new listingAgent
PUT/api/v1/properties/{id}Update listingOwner Agent
DELETE/api/v1/properties/{id}Deactivate listingOwner Agent
POST/api/v1/properties/{id}/photosUpload photo (returns presigned URL)Owner Agent
GET/api/v1/properties/{id}/historyPrice & status historyOptional
GET/api/v1/properties/{id}/comparablesComparable salesOptional
GET/api/v1/properties/{id}/neighborhoodNeighborhood dataOptional
POST/api/v1/inquiriesSubmit contact form / questionUser
POST/api/v1/showingsRequest a showingUser
GET/api/v1/users/favoritesList user's favorited propertiesUser
POST/api/v1/users/favorites/{propertyId}Add to favoritesUser
DELETE/api/v1/users/favorites/{propertyId}Remove from favoritesUser
GET/api/v1/users/saved-searchesList saved searchesUser
POST/api/v1/users/saved-searchesCreate saved searchUser
POST/api/v1/mortgage/calculateMortgage payment calculationNone
GET/api/v1/agents/{id}Agent profileOptional
POST/api/v1/rentals/applicationsSubmit rental applicationUser
POST/api/v1/documents/uploadGet presigned upload URLUser/Agent
GET/api/v1/market/trends/{geoId}Market trend dataOptional
POST/api/v1/cma/generateGenerate CMA reportAgent

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 Versioning Strategy: Use URL-based versioning (/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 CategoryKey MetricsAlert Threshold
Search PerformanceLatency (p50, p95, p99), throughput, error ratep99 > 200ms, error rate > 1%
Listing FreshnessMLS sync lag, update propagation timeSync lag > 15 minutes
Media PipelineUpload success rate, processing time, CDN hit ratioCDN hit ratio < 90%
Lead PipelineLead volume, response time, conversion rateAvg response > 30 min
ML ModelPrediction accuracy, latency, feature freshnessMdAPE > 8%
InfrastructureCPU, memory, disk, network per serviceCPU > 80% sustained
BusinessDAU, listings viewed, leads submitted, showingsDay-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

Critical Compliance Area: The Fair Housing Act (42 U.S.C. § 3601-3619) prohibits discrimination in housing based on race, color, national origin, religion, sex, familial status, and disability. The platform must ensure that:
  • 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

RequirementImplementationStatus
Keyboard NavigationAll interactive elements focusable and operable via keyboardMust Have
Screen Reader SupportARIA labels on all images, form fields, and interactive elementsMust Have
Color ContrastMinimum 4.5:1 contrast ratio for text, 3:1 for UI componentsMust Have
Alt TextEvery listing photo has descriptive alt textMust Have
Virtual Tour AccessibilityAudio descriptions for 3D tours, text alternatives for floor plansShould Have
CaptionsVideo walkthroughs include closed captionsMust Have
Error HandlingForm errors announced to screen readersMust 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 TypeScopeCount TargetExecution Time
Unit TestsBusiness logic, calculations, data transformations2,000+< 5 minutes
Integration TestsService-to-DB, service-to-service, API contract tests500+< 15 minutes
Contract TestsAPI request/response schemas between frontend and backend200+< 5 minutes
End-to-End TestsCritical user journeys (search, view, contact, schedule)50+< 30 minutes
Performance TestsLoad testing, stress testing for search and listing pages20+ scenarios< 60 minutes
ML Model TestsPrediction accuracy, feature drift, A/B test evaluationContinuousNightly 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"));
    }
}
Geospatial Testing: Geospatial tests require careful handling of coordinate precision and projection systems. Use a test database with PostGIS enabled and test with real-world coordinate data from known locations. Test edge cases: properties exactly on the polygon boundary, coordinates that cross the antimeridian (longitude ±180°), and properties at different latitudes where "1 mile" covers different longitude ranges.

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.

Real Estate Listing & Search Platform — Senior+ System Design Guide | Ayodhyya