An OpenFlights-based ingestion system that reads .dat datasets, publishes typed Kafka
messages, consumes them, and stores the normalized result in PostgreSQL.
Stack: Java 25, Spring Boot, Spring Kafka, Spring Data JPA, PostgreSQL, Swagger/OpenAPI, Cucumber, Testcontainers
Start PostgreSQL and Kafka for local development with:
docker compose -f openflights/compose.yaml up -dStop them with:
docker compose -f openflights/compose.yaml downIf you want a clean local reset of persisted Docker data:
docker compose -f openflights/compose.yaml down -vThis stack exposes:
| Service | URL / Port | Notes |
|---|---|---|
| PostgreSQL | localhost:5432 |
database/user/password are all openflights |
| Kafka | localhost:9092 |
single-node KRaft broker |
The OpenFlights runtime is split into three Spring Boot applications:
mvn -f openflights/openflights-kafka-producer/pom.xml spring-boot:run
mvn -f openflights/openflights-kafka-consumer/pom.xml spring-boot:run
mvn -f openflights/openflights-app/pom.xml spring-boot:run| Module | Port | Main purpose | Swagger / API docs |
|---|---|---|---|
openflights-kafka-producer |
8051 |
reads OpenFlights .dat files and publishes typed Kafka records |
http://localhost:8051/swagger-ui.html / http://localhost:8051/api-docs |
openflights-kafka-consumer |
8052 |
consumes typed Kafka records and persists them into PostgreSQL | no Swagger surface |
openflights-app |
8050 |
operational/admin HTTP endpoints for persisted data | http://localhost:8050/swagger-ui.html / http://localhost:8050/api-docs |
Important runtime responsibilities:
OpenFlightsDataConfigurationwires the framework-lightopenflights-datareaders/parsers into Spring beans and provides the dedicated import executor.OpenFlightsFileImportServicereads one dataset at a time and publishes records in bounded parallel chunks.KafkaMessageProducerroutes each record type to the correct Kafka topic and derives stable Kafka keys.OpenFlightsPersistenceServiceowns consumer-side normalization, idempotent persistence, placeholder reference handling, and out-of-order route ingestion recovery.OpenFlightsPlaceholderWriteServicewrites temporary airline/airport/country placeholders in separate transactions so a concurrent duplicate placeholder insert does not abort the outer route transaction.AdminDataControllerexposes the PostgreSQL cleanup endpoint fromopenflights-app.SqlController/SqlService/SqlRepositorypower the admin SQL console inopenflights-app, where the backend returns structured query data and the browser renders the result table locally.
openflights-kafka-consumer and openflights-app both reuse the same datasource/JPA defaults from
openflights-jpa via classpath:openflights-jpa-defaults.yaml, so the Postgres connection settings
stay centralized.
Import data through the producer endpoints in this order:
- countries
- airlines
- airports
- planes
- routes
Endpoints:
| Dataset | Endpoint |
|---|---|
| countries | POST /api/v1/openflights/import/countries |
| airlines | POST /api/v1/openflights/import/airlines |
| airports | POST /api/v1/openflights/import/airports |
| planes | POST /api/v1/openflights/import/planes |
| routes | POST /api/v1/openflights/import/routes |
The preferred runtime order is still the list above, even though route ingestion can now recover from missing airline and airport references by creating temporary placeholders and later overwriting them with real reference data.
Kafka topic layout:
| Topic | Default partitions | Notes |
|---|---|---|
openflights.country |
1 |
reference data |
openflights.airline |
1 |
reference data |
openflights.airport |
1 |
reference data |
openflights.plane |
1 |
reference data |
openflights.route |
8 |
higher-throughput route ingestion |
The route consumer also uses 8 listener threads by default. Those defaults can be adjusted in:
- Producer application.yaml
- Consumer application.yaml
openflights-app exposes:
DELETE /api/v1/openflights/admin/data
That endpoint deletes persisted PostgreSQL rows only. It does not reset Kafka topics, consumer offsets, or any in-flight retry state.
openflights-app also exposes a small SQL console at:
http://localhost:8050/index.html
Current design notes:
- the editor uses CodeMirror with SQL highlighting
- the browser calls
POST /api/v1/openflights/admin/sql - the backend returns structured JSON, not pre-rendered HTML
- the browser renders the result table, toolbar, paging controls, and messages
- paging is database-backed for row-returning queries, so PostgreSQL is asked for the current page window instead of returning the full result set to the app
That split keeps repositories focused on SQL execution and keeps presentation logic in the frontend where it belongs.
| Module | Responsibility |
|---|---|
openflights-api |
shared records, Kafka topic constants, low-level OpenFlights value parsing |
openflights-data |
framework-light .dat readers and parsers |
openflights-jpa |
JPA entities, repositories, mappers, normalizers, shared datasource defaults |
openflights-kafka-producer |
import endpoints, file-import orchestration, Kafka publishing |
openflights-kafka-consumer |
typed Kafka listeners, persistence orchestration, placeholder repair |
openflights-app |
admin/operational HTTP endpoints for persisted data |
openflights-testing |
Cucumber end-to-end tests with Testcontainers |
For the full runtime and data-model breakdown, see ARCHITECTURE.md.
openflights-testing runs end-to-end Cucumber scenarios with Testcontainers and uses the same route
topic partition count and route consumer concurrency as the normal runtime configuration.
The full-ingestion Cucumber scenario currently expects the final PostgreSQL dataset to reach:
- countries:
260 - airlines:
6162 - airports:
7810 - planes:
246 - routes:
67663 - routeEquipmentCodes:
93231
Those values are asserted directly in OpenFlightsIngestion.feature and cross-checked by the calculator test support.
Per-type and total ingestion timings reported by the Cucumber suite are observational only. They vary from machine to machine depending on CPU, Docker load, and local PostgreSQL/Kafka performance, and they measure when PostgreSQL reaches the expected dataset state rather than raw producer HTTP response time.