Real Deal
An automotive platform that is both the reference and the market: official specifications and prices for every model, and dealer-to-dealer, dealer-to-customer and customer-to-customer trade at fixed prices or under a live auction clock.
- Traffic measured in millions of requests.
Context
Real Deal carries every direction of vehicle trade on one platform. Dealers sell to other dealers, dealers sell to private buyers, and private sellers sell to each other, so a listing can have a business or a person behind it and a buyer can be either as well. Underneath all of it sits an official catalogue: every brand, model and trim with its full manufacturer specifications and its official price, kept current as prices move, and a side-by-side comparison that lines two cars up specification for specification. That catalogue is a product in its own right, because a buyer deciding what to want reads it before any listing, and it is also the reference the marketplace hangs off, since a listing points at a model rather than restating it. On the consumer side that means discovery over new and used cars: search, filtering, specifications, pricing, comparison and contact with whoever is selling. On the dealer side it means a business system covering inventory, listings, sales operations and trading between automotive businesses. Alongside all of it there is a live vehicle auction, running on its own stock, where dealers and private bidders compete against each other and against a closing clock. Five client surfaces (a buyer app, a dealer app, a buyer website, a dealer website and an admin dashboard) run against one backend API, serving car buyers, dealers, automotive businesses and platform operators.
The problem
The platform carries three directions of trade over one shared, continuously changing inventory, and a live auction over a second one. Four properties turn that from a large catalogue into an engineering problem. Who is selling is part of the model rather than a label on it, because a business and a private person listing the same car are not the same transaction. A listing that outlives the sale it describes erodes trust in the entire marketplace, so availability has to behave as part of the query rather than as a display detail. A serious car listing carries many high-resolution images, which at marketplace scale is a delivery and cost problem before it is a user-experience one. And an auction is the one part of the platform that cannot be approximately right: bids arriving together have to be ordered, everyone watching a lot has to see the same current price at the same time, and a bid landing after the clock has stopped is worse than one rejected before it.
The solution
One platform carries every pairing of buyer and seller: discovery and search for whoever is looking, dealer inventory, listing management and sales operations for the businesses, private listings for people selling their own car, and trading between automotive businesses, all over one official catalogue of brands, models, trims, full specifications and official prices, which is also what powers specification comparison and what every listing references rather than duplicates. Two mobile applications, two websites and an admin dashboard run against a single backend rather than against separate per-audience systems. The auction sits beside the marketplace on its own stock: a live timed sale where dealers and private bidders are in the same room, the current price pushed to every watching client over WebSockets rather than waited for, and the lot closing on a clock. Search is treated as the core capability, filtering across brand, model, price, year, specifications, fuel type, vehicle characteristics, budget, availability and dealer inventory, with budget-based discovery as a first-class entry point alongside attribute filtering. Search runs on the inventory database itself rather than on a separate search engine, with the filter counts cached. That splits the problem in two along its cost line: the query has to be correct and current, so it reads from the source of truth, while the counts beside every filter option are expensive and change slowly, so they are computed ahead and served from Redis on a timer. The request path runs client → API → database → cache → storage → CDN, with media delivery handled as its own upload-to-delivery pipeline, and the auction adds a push path back out to the clients that the rest of the platform does not need.
Architecture
The topology is a single backend and API ecosystem fronting five client surfaces: a buyer mobile app, a buyer website, a dealer mobile app, a dealer website and an admin dashboard. That shape follows from the inventory itself: consumer listings and dealer trading assets are the same vehicles under different visibility, pricing and workflow rules, so splitting them across separate backends would mean keeping two copies of one truth in agreement. The backend is Laravel on Octane over MySQL; the two mobile applications are Flutter and the two websites and the admin dashboard are Next.js. Behind the API the chain is MySQL, a cache, Amazon S3 and CloudFront, which fits a read path dominated by search traffic and a media path dominated by listing images. Vehicle media leaves the API for S3 and reaches buyers through CloudFront rather than through the API, keeping image bytes off the request path that search shares. Search sits on MySQL rather than beside it in a search cluster, which is a decision the read path pays for and the write path is spared: there is no index to keep in step with inventory that changes all day, and the cost lands instead on the query and the indexes underneath it. The auction is the exception to the whole shape. Everything else here is request and response; a live sale has to push, so bids travel out through Pusher over WebSockets to every client watching the lot. Redis holds the cached facet counts on a timer. The platform runs on AWS on an Amazon EKS cluster.
Read this diagram as text
- Dealer app + website
- Flutter app and Next.js website. Dealer inventory, listing management, sales operations, performance information and B2B vehicle trading workflows.
- Admin dashboard
- Next.js dashboard where platform operators manage users, vendors, vehicles, brands, models, listings, pricing, inquiries and marketplace configuration.
- Laravel API
- One Laravel API serving all five surfaces, holding inventory, search, pricing, media and business logic. Search resolves here against MySQL rather than against a separate search cluster.
- MySQL
- Vehicle inventory, pricing and catalogue data, taking constant dealer writes underneath constant consumer search reads. It answers the search queries too, since there is no separate index.
- Official catalogue
- Every brand, model and trim with full manufacturer specifications and the official price, kept current as prices move. The reference the rest of the platform reads from: a listing points at a model rather than restating it, specification filters run on it, and comparison depends on it being normalised across manufacturers who each publish differently.
- Redis
- Holds the filter counts that sit beside every search facet, expiring them on a timer rather than busting them on a write. The counts are the half of the query that can afford to be a little stale.
- Auction engine
- Runs the live timed sale: accepts and orders bids, holds the current price, and closes each lot on its clock.
- Amazon S3
- Origin for dealer-uploaded vehicle images, of which a serious listing carries many at high resolution.
- Auction stock
- Vehicles entered for auction, held apart from the marketplace inventory so bid concurrency stays clear of the listing tables consumer search contends for.
- Pusher
- Carries each accepted bid out over WebSockets to every client watching the lot, so the current price arrives rather than being polled for.
- CloudFront
- Delivers listing media to buyer surfaces and keeps image bytes off the API path that search shares.
- Buyer app + website
- Flutter app and Next.js website. Consumer discovery, multi-attribute search, vehicle detail, comparison, budget-based discovery and dealer inquiries.
- Buyer app + website → Laravel API (search, listings, inquiries)
- Dealer app + website → Laravel API (inventory, pricing, trading)
- Admin dashboard → Laravel API (catalogue & configuration)
- Laravel API → MySQL (listings and inventory)
- Laravel API → Official catalogue (models, specs, official prices)
- Laravel API → Redis (facet counts)
- Laravel API → Amazon S3 (vehicle media)
- Amazon S3 → CloudFront (origin)
- Laravel API → Auction engine (bids, lots)
- Auction engine → Auction stock (read / write)
- Auction engine → Pusher (publish bid)
- CloudFront → Buyer app + website (image delivery)
What makes it interesting
The parts a system like this is actually judged on.
- Every direction of trade runs over one listing model, which makes the seller part of the schema rather than a detail hung off it. A dealer selling to another dealer, a dealer selling to a private buyer, and a person selling their own car to another person are three different transactions wearing the same shape. What separates them is not the vehicle: it is who may see the listing, what the platform is understood to have checked, and what a buyer can expect if something is wrong. A model that treats a listing as a car with a price loses that distinction at the point where it matters most.
- Comparison is a normalisation problem wearing the clothes of a feature. Standing two cars side by side and lining their specifications up row for row needs one schema that no manufacturer actually publishes to, so every brand’s own way of describing a gearbox, a trim level or a consumption figure has to be mapped onto a common shape first. The work is invisible when it succeeds and obvious the moment two rows fail to line up, and it is the same work that makes filtering by specification possible at all, because a filter is only ever as good as the field beneath it.
- An official price is a time series rather than a number, which is easy to miss until it is wrong. Manufacturer prices move, so the catalogue has to say what a car costs now while staying honest about what it cost before. That also fixes the relationship between the two halves of the platform: the official price is the reference, and what a dealer or a private seller is actually asking is a separate figure that only means something when read next to it.
- The auction cuts across the same matrix rather than sitting outside it, because a lot can be put in front of dealers or in front of everybody, and it is the one place where trade buyers and private bidders are in direct competition for the same vehicle. That makes who is allowed into a room a property of the lot, and it has to be enforced on the live push path as well as on the page, since a price update sent to a client that should not be watching is the same leak whichever channel carries it.
- The same vehicle record participates in two economies at once, as a consumer listing and as a dealer trading asset, with different visibility, pricing and workflow rules over one shared inventory rather than two copies of it. How those two views are modelled and kept in agreement is not documented.
- Availability is part of the query rather than a rendering detail: a sold vehicle has to leave results immediately, which puts correctness directly against the aggressive caching that traffic in the millions of requests demands.
- Budget-based discovery inverts the usual index assumptions, putting a range-driven query shape next to high-dimensional attribute filtering whose predicates are barely selective alone. The counts beside each filter option are a second query problem stacked on the first, and they are the half that got cached, on a plain timer rather than invalidated by dealer writes. That asymmetry is the whole argument: a count a few minutes stale costs nothing, while a result set a few minutes stale shows a car that has already sold.
- The write path of dealers updating inventory and pricing continuously and the read path of constant consumer search want opposite things from the same data, so the schema, the indexes and the caching that suit one side work against the other.
- The auction is the one place on the platform that cannot be eventually consistent. Two bids arriving in the same instant have to be ordered and one of them told it lost, a bid accepted after the clock has stopped is worse than one rejected before it, and every client watching the lot has to be shown the same current price at the same moment. Nothing else here has that shape: a search result a second out of date is fine, a bid a second out of date is a dispute.
- Auction stock is held apart from the marketplace inventory rather than as a flag on it, so the platform carries two vehicle populations under different rules. That keeps auction concurrency away from the listing tables consumer search is already contending for, at the price of a second place where a vehicle can exist.
Engineering challenges
- One specification schema across manufacturers who each publish their own, which is what comparison and spec filtering both rest on
- Official prices that move, so the catalogue carries a history rather than a value
- Search over a moving corpus: high-dimensional filtering while inventory, pricing and availability change constantly.
- Media weight: many high-resolution images per listing across a large inventory, making storage, processing and delivery a cost centre.
- Cache invalidation: traffic requires aggressive caching, and marketplace trust requires that a cached result never contains a sold vehicle.
- Read/write contention: constant dealer writes against constant consumer reads on the same records.
- Two marketplaces, one inventory: consumer listings and B2B trading assets are the same vehicles under different rules.
- Five client surfaces (two apps, two websites and an admin) on one API, without it degrading into five APIs.
- Sustaining performance as both traffic and dataset grow, against traffic measured in millions of requests.
- Live auction concurrency: ordering simultaneous bids correctly, closing a lot exactly on time, and pushing the same current price to every watching client at once.
Integrations
- Paymob: payment gateway
- Firebase: push notification delivery to the mobile apps
- SMS Masr: SMS gateway for messages leaving the platform as text
- OAuth: third-party sign-in; which providers is not recorded
- Pusher: live auction bid delivery over WebSockets
Technology
- Laravel
- Laravel Octane
- MySQL
- Flutter
- Next.js
- OAuth
- Paymob
- Firebase
- SMS Masr
- Pusher
- WebSockets
- Redis
- AWS
- Amazon EKS
- Amazon S3
- Amazon CloudFront
- Docker