A Spring Boot POC for a shipping line backend. Internal operations teams can create voyages between ports and book containers (freight orders) onto those voyages.
Port ←── Voyage ──→ Port
│
│ Vessel
│
FreightOrder
│
Container (20ft / 40ft, DRY / REEFER / …)
Key entities:
- Port – identified by UN/LOCODE (e.g.
AEJEAfor Jebel Ali) - Vessel – identified by 7-digit IMO number
- Container – ISO 6346 code, size (20/40 foot), type (DRY, REEFER, OPEN_TOP, FLAT_RACK, TANK)
- Voyage – a scheduled vessel trip from departure port → arrival port
- FreightOrder – books a container onto a voyage, placed by an internal team member
| 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 with database freightops and credentials freight/freight.
# From the project root
./mvnw clean install
# Run the app
./mvnw spring-boot:runThe server starts on http://localhost:8080. On first boot, Hibernate creates the tables and data.sql seeds sample ports, a vessel, and a few containers.
Create a voyage first (there's no controller for this yet — that's your job!), or insert one directly:
-- Connect to postgres: docker exec -it freightops-db psql -U freight -d freightops
INSERT INTO voyages (voyage_number, vessel_id, departure_port_id, arrival_port_id,
departure_time, arrival_time, status, created_at, updated_at)
VALUES ('VOY-2025-001', 1, 1, 2, '2025-04-01 08:00', '2025-04-15 18:00', 'PLANNED', NOW(), NOW());Create a freight order:
curl -X POST http://localhost:8080/api/v1/freight-orders \
-H 'Content-Type: application/json' \
-d '{
"voyageId": 1,
"containerId": 1,
"orderedBy": "ops-team",
"notes": "Fragile cargo"
}'List all freight orders:
curl http://localhost:8080/api/v1/freight-ordersGet a single order:
curl http://localhost:8080/api/v1/freight-orders/1Tests use an H2 in-memory database — no PostgreSQL needed.
./mvnw testLook at FreightOrderControllerTest.java for a working example of how to write integration tests with MockMvc and JUnit 5 (Jupiter).
The API is documented with OpenAPI/Swagger. Once the application is running, you can access the interactive Swagger UI at: http://localhost:8080/swagger-ui/index.html
From there, you can explore endpoints, see request/response schemas, and try out the API directly from the browser.
💡 Note: The older /swagger-ui.html path is automatically redirected to /swagger-ui/index.html.
src/main/java/com/shipping/freightops/
├── FreightOpsApplication.java # Entry point
├── config/
│ └── GlobalExceptionHandler.java # Centralized error handling
├── controller/
│ └── FreightOrderController.java # ★ Sample controller — follow this pattern
├── dto/
│ ├── CreateFreightOrderRequest.java
│ └── FreightOrderResponse.java
├── entity/
│ ├── BaseEntity.java # Shared id + audit fields
│ ├── Container.java
│ ├── FreightOrder.java
│ ├── Port.java
│ ├── Vessel.java
│ └── Voyage.java
├── enums/
│ ├── ContainerSize.java
│ ├── ContainerType.java
│ ├── OrderStatus.java
│ └── VoyageStatus.java
├── repository/
│ ├── ContainerRepository.java
│ ├── FreightOrderRepository.java
│ ├── PortRepository.java
│ ├── VesselRepository.java
│ └── VoyageRepository.java
└── service/
└── FreightOrderService.java
This POC has one working controller (FreightOrderController). Your tasks:
- VoyageController – CRUD for voyages (create a voyage between two ports on a vessel)
- ContainerController – CRUD for containers
- PortController – CRUD for ports
- VesselController – CRUD for vessels
For each controller, follow the same pattern:
- Create a Request DTO (e.g.
CreateVoyageRequest) with validation annotations - Create a Response DTO (e.g.
VoyageResponse) with afromEntity()factory method - Create a Service class with business logic
- Create a Controller with REST endpoints
- Write a test class following
FreightOrderControllerTestas a template
This project uses Google Java Format. The Maven build auto-formats on compile via the fmt-maven-plugin.
To manually format:
./mvnw fmt:formatTo check formatting without changing files:
./mvnw fmt:checkIDE setup:
- IntelliJ: Install the "google-java-format" plugin → Settings → google-java-format → Enable
- VS Code: Use the "Google Java Format" extension
| Command | Description |
|---|---|
mvn clean install |
Build + run tests |
mvn clean verify |
Build + test + coverage report |
mvn spring-boot:run |
Start the app |
mvn test |
Run tests only (H2, no Docker) |
mvn fmt:format |
Format code (Google style) |
mvn 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 |
After mvn clean verify, open target/site/jacoco/index.html to browse the coverage report
locally.
Every push to main/develop and every PR triggers the CI pipeline:
- Build & Test —
mvn clean verifywith JDK 21 - Format Check —
mvn fmt:checkfails the build if code isn't Google-formatted - Test Coverage — JaCoCo generates a report, posted as a PR comment with coverage diff
- Test Results — Surefire results published as a GitHub check
Coverage reports are uploaded as build artifacts and retained for 14 days. On PRs, the bot posts a comment with overall coverage and per-file diff — minimum thresholds are 40% overall and 60% on changed files.
To run the same checks locally before pushing:
mvn clean verify # build + test + coverage report
mvn fmt:check # format check (no changes)Coverage HTML report is generated at target/site/jacoco/index.html.
Ready to pick up a task? See CONTRIBUTING.md for workflow, branch naming, and PR guidelines.
- Phase 1 — Core CRUD and foundations: ISSUES.md
- Phase 2 — Pricing, invoicing, notifications, vessel planning, finance, commissions, barcode tracking, and AI pricing: ISSUES-PHASE2.md
All issues use a domain-prefixed naming convention (e.g. PRC-001, VPL-002) so dependencies are
easy to follow. See the naming table at the top of each issues file.
For the big picture on where this project is headed, see the ROADMAP.md.
- Don't skip the DTO layer — never expose JPA entities directly in REST responses.
- Use
@Transactional(readOnly = true)on read-only service methods for better performance. - Fetch type is LAZY on all
@ManyToOnerelations — be mindful ofLazyInitializationExceptionif you access relations outside a transaction. - Validation is handled via Jakarta annotations (
@NotNull,@NotBlank, etc.) — theGlobalExceptionHandlerconverts these into clean 400 responses automatically. - Check existing repositories for query method naming conventions before writing custom
@Query.