A Spring Boot backend for a shipping line operations platform. Internal teams manage vessels, voyages, freight orders, customers, agents, and invoices. Features include TEU-based capacity control, PDF invoice generation with QR tracking codes, AI-powered price suggestions, and a provider-agnostic LLM abstraction layer.
Port ←── Voyage ──→ Port
│
Vessel (TEU capacity / DWT)
│
FreightOrder ──→ Container (TEU-based)
│ or BulkCargo (tonnes-based) [planned]
│
Customer
Operator
Agent
Core entities:
| Entity | Key field(s) | Notes |
|---|---|---|
| Port | unlocode (5 chars, UN/LOCODE) |
e.g. AEJEA for Jebel Ali |
| Vessel | imoNumber (7 digits) |
Carries capacityTeu; DWT planned |
| Container | containerCode (ISO 6346, 11 chars) |
Size: 20 / 40 ft · Type: DRY, REEFER, … |
| Voyage | voyageNumber (unique) |
departure → arrival port, vessel, status |
| FreightOrder | FK to Voyage + Container + Customer + Operator | Priced from VoyagePrice, supports discounts |
| VoyagePrice | (voyageId, containerSize) |
Base price in USD per container size |
| Customer | companyName, email |
Linked to freight orders |
| Agent | name, agentType, commissionPercent |
Freight forwarder or port agent |
| VesselOwner | name, sharePercent |
Multi-owner support |
| Invoice | FK to FreightOrder | PDF with embedded QR code |
| TrackingEvent | FK to FreightOrder | Event log for shipment lifecycle |
| Tool | Version |
|---|---|
| Java (JDK) | 21+ |
| Maven | 3.8+ |
| Docker | 20+ |
| Docker Compose | 2+ |
cd docker
docker compose up -dThis creates a PostgreSQL 16 instance at localhost:5432 (database freightops,
credentials freight/freight).
./mvnw clean install
./mvnw spring-boot:runThe server starts on http://localhost:8080. On first boot, data.sql seeds sample ports, a
vessel, and containers.
Create a vessel:
curl -X POST http://localhost:8080/api/v1/vessels \
-H 'Content-Type: application/json' \
-d '{"name": "MSC Gülsün", "imoNumber": "9811000", "capacityTeu": 23756}'Create a voyage:
curl -X POST http://localhost:8080/api/v1/voyages \
-H 'Content-Type: application/json' \
-d '{
"voyageNumber": "VOY-2026-001",
"vesselId": 1,
"departurePortId": 1,
"arrivalPortId": 2,
"departureTime": "2026-05-01T08:00:00",
"arrivalTime": "2026-05-15T18:00:00"
}'Create a freight order:
curl -X POST http://localhost:8080/api/v1/freight-orders \
-H 'Content-Type: application/json' \
-d '{
"voyageId": 1,
"containerId": 1,
"customerId": 1,
"operatorId": 1,
"notes": "Fragile cargo"
}'Generate an invoice PDF:
curl http://localhost:8080/api/v1/freight-orders/1/invoice --output invoice.pdfTests use an H2 in-memory database — no PostgreSQL needed.
./mvnw testFor the full build with coverage report:
./mvnw clean verify
# Open target/site/jacoco/index.htmlOnce the application is running: http://localhost:8080/swagger-ui/index.html
Explore all endpoints, see request/response schemas, and try the API from the browser.
src/main/java/com/shipping/freightops/
├── FreightOpsApplication.java
├── ai/ # LLM abstraction (Claude, OpenAI, NoOp implementations)
├── config/
│ ├── GlobalExceptionHandler.java
│ ├── PageableConfig.java
│ └── BookingProperties.java # TEU cutoff threshold configuration
├── controller/
│ ├── FreightOrderController.java # ★ reference implementation
│ ├── VoyageController.java
│ ├── VesselController.java
│ ├── ContainerController.java
│ ├── PortController.java
│ ├── CustomerController.java
│ ├── AgentController.java
│ ├── VesselOwnerController.java
│ ├── InvoiceController.java
│ └── TrackingController.java
├── dto/ # Request / Response DTOs — never expose entities directly
├── entity/
│ ├── BaseEntity.java # Shared id + audit timestamps
│ ├── Port.java
│ ├── Vessel.java
│ ├── Container.java
│ ├── Voyage.java
│ ├── FreightOrder.java
│ ├── VoyagePrice.java
│ ├── VoyageCost.java
│ ├── Customer.java
│ ├── Agent.java
│ ├── VesselOwner.java
│ ├── Invoice.java
│ └── TrackingEvent.java
├── enums/
│ ├── ContainerSize.java
│ ├── ContainerType.java
│ ├── OrderStatus.java
│ ├── VoyageStatus.java
│ └── AgentType.java
├── exception/
│ └── BadRequestException.java
├── repository/ # Spring Data JPA repositories
└── service/ # Business logic
This project uses Google Java Format. The Maven
build auto-formats on compile via fmt-maven-plugin.
./mvnw fmt:format # reformat all sources
./mvnw fmt:check # check without changing (used in CI)IDE setup:
- IntelliJ: Install the "google-java-format" plugin → Settings → google-java-format → Enable; also enable annotation processing for Lombok
- VS Code: Install "Google Java Format" and "Lombok Annotations Support" extensions
| Command | Description |
|---|---|
./mvnw clean install |
Build + run tests |
./mvnw clean verify |
Build + test + coverage report |
./mvnw spring-boot:run |
Start the app |
./mvnw test |
Run tests only (H2, no Docker) |
./mvnw fmt:format |
Format code (Google style) |
./mvnw fmt:check |
Check format without changing |
docker compose -f docker/docker-compose.yml up -d |
Start PostgreSQL |
docker compose -f docker/docker-compose.yml down -v |
Stop + delete data |
Every push to master/develop and every PR triggers:
- Build & Test —
./mvnw clean verifywith JDK 21 - Format Check —
./mvnw fmt:checkfails if code is not Google-formatted - Test Coverage — JaCoCo report posted as a PR comment; minimums: 40% overall, 60% on changed files
- Test Results — Surefire results published as a GitHub check
Coverage reports are uploaded as build artifacts (14-day retention).
Ready to pick up a task? See CONTRIBUTING.md for workflow, branch naming, and PR guidelines.
- Phase 1 — Core CRUD and foundations: docs/ISSUES.md
- Phase 2 — Pricing, invoicing, vessel planning, finance, tracking, AI pricing: docs/ISSUES-PHASE2.md
- Phase 3 — Infrastructure hardening, data model cleanup, bulk cargo: docs/ISSUES-PHASE3.md
All issues use a domain-prefixed naming convention (INF-001, CRG-001, etc.) so dependencies
are easy to follow. See the naming tables at the top of each issues file.
For the big picture see docs/ROADMAP.md. For a non-technical overview of all flows and the data model see docs/stakeholder-overview.md.
- DTO layer is mandatory — never expose JPA entities directly in REST responses.
@Transactional(readOnly = true)on read-only service methods.- All
@ManyToOneare LAZY — always access associations inside a@Transactionalboundary to avoidLazyInitializationException. - Validation via Jakarta annotations;
GlobalExceptionHandlerconverts violations to clean 400 responses automatically. - Not-found cases must throw
ResponseStatusException(NOT_FOUND), notIllegalArgumentException— the former becomes 404, the latter becomes 500. - Format before committing —
./mvnw fmt:format; CI will reject unformatted code.