Wanneer je een autohandel runt in Azerbeidzjan en schadevoertuigen importeert uit de Verenigde Staten, zien je softwareproblemen er anders uit dan die van een startup in Silicon Valley. Je optimaliseert niet voor een miljoen gelijktijdige gebruikers. Je optimaliseert voor duidelijkheid, uptime en het vermogen om dingen om middernacht zelf op te lossen terwijl je coördineert met een veilinghuis dat twaalf tijdzones verderop zit. Dat is precies de situatie waarin ik me bevond toen ik AutoMakler bouwde. Het platform regelt alles, van live auction scraping en Carfax-opvragingen tot leveringsschattingen en betalingsverwerking. Het is een echt productiesysteem dat echte klanten bedient, en het draait op wat de meeste ontwikkelaars een agressief saaie stack zouden noemen.
De stack die niemand wil pitchen
Er is geen React. Geen Vue. Geen Redis, geen Celery en geen WebSocket-server. De backend is FastAPI met gewone Python. De database is PostgreSQL. De frontend is server-gerenderde HTML met Jinja2-templates, Bootstrap en een snufje vanilla JavaScript. Voor scraping gebruik ik Playwright. Alles draait als een enkel Python-proces dat HTML rechtstreeks serveert.
Er is geen build-stap. Er zijn geen node_modules-mappen om te controleren, geen transpilers om te configureren en geen constante wisselingen van frontend-frameworks om bij te houden. Wanneer ik deploy, verplaats ik Python-bestanden en templates, in plaats van het orchestreren van een pipeline van bundlers. Die eenvoud is geen compromis. Het is juist het hele punt.
Hoe je taken in een wachtrij zet zonder een message broker
Het scrapen van een live autoveiling kan niet synchroon gebeuren. Eén enkele scrape kan enkele seconden duren terwijl Playwright de pagina laadt, JavaScript uitvoert en de gegevens extraheert. De gebruiker blokkeren terwijl dit gebeurt, is geen optie. Het standaardrecept schrijft voor om Redis te installeren, Celery te configureren en een worker pool op te spinnen. Ik heb dat allemaal overgeslagen.
In plaats daarvan gebruikt AutoMakler Postgres als zijn eigen taakwachtrij. Wanneer een gebruiker een scrape activeert, schrijft de applicatie een nieuwe rij in een tasks-tabel met de status pending. Een asyncio-achtergrondtaak pakt die rij op en start de browser-scrape. Ondertussen controleert de browser elke drie seconden een lichtgewicht endpoint om de status te controleren. Wanneer de rij wordt bijgewerkt naar completed, ververst de pagina en worden de resultaten getoond.
Dit patroon werkt omdat het polling-interval kort genoeg is om responsief aan te voelen, maar lang genoeg om de server niet te overbelasten. Drie seconden is een eeuwigheid voor een computer en nauwelijks merkbaar voor een mens die wacht op een externe veilingwebsite. De database handelt concurrency van nature af, en omdat de taken gewoon rijen in Postgres zijn, kan ik de wachtrij inspecteren met een eenvoudige SQL-query in plaats van door Celery-logs of Redis-keys te graven.
De server draaiende houden zonder een worker pool
Browserautomatisering is geheugenintensief. Start je te veel Playwright-instanties tegelijkertijd op, dan stort je server in. De conventionele oplossing is een beheerde worker pool met concurrency-limieten, vaak ondersteund door diezelfde combinatie van Redis en Celery. Ik gebruik één regel Python: een asyncio.Semaphore.
De semaphore beperkt hoeveel gelijktijdige browser-instanties er kunnen draaien. Wanneer er een nieuw scrape-verzoek binnenkomt, pakt het ofwel direct een plek in, of het wacht tot er een vrijkomt. Dit alles gebeurt binnen hetzelfde proces. Er is geen externe orchestrator die kan falen, geen worker-proces dat stilletjes kan sterven en geen extra infrastructuur om te monitoren. Mijn geheugengebruik blijft voorspelbaar, en de code die de server beschermt, staat direct naast de code die hem gebruikt, in plaats van verborgen te zijn in een deployment manifest.
Geld routeren met één callback-URL
Betalingsverwerking introduceerde een beperking die ik niet kon veranderen. Mijn payment gateway staat precies één callback-URL toe per merchant-account, maar ik moest transacties voor twee aparte projecten via datzelfde account verwerken. Het bouwen van een tweede merchant-profiel zou extra kosten, extra compliance en extra papierwerk betekenen waar een kleine autohandel geen tijd voor heeft.
De oplossing was om de projectnaam direct in de order ID-string te coderen voordat de klant naar de gateway werd gestuurd. Wanneer de callback mijn server bereikt, decodeert AutoMakler die ID, identificeert bij welk project de betaling hoort en stuurt de melding naar de juiste interne handler. De bestaande logica bleef ongewijzigd. Dit is additief ontwerp: ik heb de betalingsflow niet herschreven, ik heb alleen de identifier voorzien van iets meer context. Het is het soort hack dat achteraf logisch lijkt, maar uren aan architecturale gymnastiek bespaart.
Chat die werkt zonder WebSockets
Klantenservice-chats zijn meestal het punt waarop engineers toegeven en WebSockets toevoegen. Ik had in-app messaging nodig, maar ik wilde de impact op de infrastructuur ook minimaal houden. Daarom heb ik dezelfde polling-strategie hergebruikt die de veiling-scrapes aanstuurt.
Berichten worden opgeslagen in Postgres. Wanneer een gebruiker een bericht stuurt, wordt dit naar de tabel geschreven. De client pollt voor updates, en de UI weerspiegelt nieuwe berichten en leesbevestigingen bijna in real-time. Om dit snel te houden, zelfs naarmate de conversatietabel groeit, heb ik een Postgres partiële index toegevoegd die alleen ongelezen berichten voor actieve gesprekken beslaat. De database verspilt geen rekenkracht aan het scannen van oude geschiedenis, en de query planner kan de meeste chat-lookups afhandelen met een nauwe index range scan.
Voor een supportchat waar een paar seconden latentie acceptabel is, is dit volkomen adequaat. De gebruikers krijgen de feedback die ze nodig hebben, en ik hoefde nooit een verouderde WebSocket-verbinding te debuggen of een aparte socketserver te beheren.
De eerlijke nadelen
Deze architectuur vereist echte afwegingen, en doen alsof het anders is, zou onjuist zijn. Polling is 'chatty'. Elke drie seconden raakt elke actieve client de server aan. De bandbreedte en de query-belasting zijn hoger dan wat een persistente socketverbinding zou vereisen. Als het Python-proces herstart, sterft elke lopende achtergrondtaak onmiddellijk omdat er geen externe worker is om deze weer op te pakken. Ik accepteer dit omdat de taken klein zijn en de kosten van een retry laag zijn. Een mislukte browser-scrape kan simpelweg door de gebruiker opnieuw worden getriggerd.
Er zit ook een limiet aan deze aanpak. Als AutoMakler ooit duizenden gelijktijdige scrapes moet verwerken, zal het single-process model met polling onder druk komen te staan. Maar dat is niet de sector waarin ik werk. Ik heb betrouwbaarheid nodig voor tientallen gelijktijdige gebruikers, niet
