Case study — production-projekt

Flight Search

Et live flysøgningssystem der kombinerer Spring Boot, Next.js, MongoDB og Google Flights-data — fra arkitektur og tekniske afvejninger til en reel production-incident jeg fejlsøgte og løste.

Arkitektur

Frontend
Next.js 16 (App Router)TypeScriptTailwind CSSClient-side validering
API-lag
Next.js Route HandlersProxy til Spring BootServer-side fetch
Backend
Spring Boot 4 / Java 21REST controllersIATA-validering
Data & eksterne services
MongoDB AtlasSerpAPI (Google Flights)7.629 lufthavne seedet

Bruger → Next.js (mdservice.uk)

└── /api/flights/* (Route Handler / proxy)

└── api.mdservice.uk (Spring Boot)

├── MongoDB Atlas — cache (TTL 30 min) + 7.629 lufthavne

└── SerpAPI → Google Flights

Tekniske beslutninger

30-minutters cache med MongoDB TTL-index

Flysøgninger cached i MongoDB med et TTL-index på 30 minutter (Duration.ofMinutes(30)). Det reducerer antallet af kald til SerpAPI markant på populære ruter, uden at data bliver forældet nok til at give brugerne forkerte priser.

booking_token / departure_token flow

Google Flights' API returnerer ikke et direkte bookinglink — kun tokens der skal sendes tilbage til SerpAPI for at få det rigtige redirect-link. Backend håndterer selve token-udvekslingen (fetchByToken) og følger derefter Googles redirect-kæde serverside med java.net.http.HttpClient, så brugeren lander direkte på det rigtige bookingsite i en ny fane — uden en mellemliggende popup eller loading-side.

IATA-validering i to lag

Frontend forhindrer at brugeren overhovedet kan søge uden at vælge en lufthavn fra dropdown (ikke bare skrive fri tekst). Backend validerer derudover at koderne matcher IATA-formatet og rent faktisk findes i databasen, samt at origin og destination ikke er identiske — så useriøse eller ugyldige requests aldrig når frem til SerpAPI og spilder API-kvote.

Production incident: SSH nede efter server-resize

Et konkret eksempel på fejlsøgning i produktion — fra symptom til root cause.

1

Symptom

Efter en resize af Linode-serveren (opgradering af RAM) stoppede GitHub Actions-deploys med at fejle: "dial tcp ***:22: i/o timeout". Hjemmesiden kørte stadig fint — kun SSH var utilgængeligt.

2

Første hypotese: Cloud Firewall

Tjekkede Linodes Cloud Firewall-regler — port 22 var korrekt åben for indgående trafik. Ingen ændring der.

3

Verificerede sshd lokalt

Via LISH-konsollen (out-of-band adgang) blev det bekræftet at sshd rent faktisk kørte og lyttede korrekt på 0.0.0.0:22 og :::22 med netstat/ps aux — så det var ikke sshd-processen selv.

4

Ledte efter et lokalt firewall

iptables var ikke installeret på serveren (Alpine Linux), og der var ingen ufw. Et nmap-scan fra Linodes egen support bekræftede at port 22 blev rapporteret som "filtered" — dvs. pakker blev droppet et sted mellem netværket og processen.

5

Root cause: nftables

apk info -e afslørede at nftables var installeret. Ruleset-dumpet viste en input-chain med "policy drop" og ingen eksplicit accept-regel for port 22 — sandsynligvis en standardregel der altid har været der, men som først blev relevant da forbindelsen skulle genetableres efter resize.

6

Løsning

Tilføjede en eksplicit accept-regel direkte i kernel-ruleset via LISH: nft insert rule inet filter input tcp dport 22 accept. SSH virkede øjeblikkeligt igen, og CI/CD-pipelinen kunne deploye normalt.