🚚 Building a Real-Time Freight Tracker in Java (Phase 1)
As a recent graduate stepping into backend development with Java Spring Boot, I wanted to build something real-world and practical. I've always been curious about how delivery tracking systems work so I started working on a Real-Time Freight Tracking Application.
This blog is a beginner-friendly, exploratory recap of how I built Phase 1: the core CRUD API backend.
🌟 Project Goal
Build a backend system that allows tracking of freight shipments across different locations with key details like origin, destination, status, and timestamps and more
🔧 Tech Stack Used
- Java 17
- Spring Boot 3.5
- Maven
- PostgreSQL (local DB)
- IntelliJ IDEA (Community Edition)
- Postman (for testing)
🧠 How the Code Works — MVC and Data Flow
🧹 Model Layer: The Domain
The core data structure is Shipment
— a JPA entity mapped to a database table. Here's what it looks like:
@Entity public class Shipment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String origin; private String destination; @Enumerated(EnumType.STRING) private ShipmentStatus status; private LocalDateTime lastUpdatedTime; @Column(nullable = false, unique = true) private String trackingNumber; private String carrier; @Enumerated(EnumType.STRING) private Priority priority; }
What I learned here:
-
@Entity
marks it as a database-mapped object -
@Id
and@GeneratedValue
handle primary key logic -
@Enumerated(EnumType.STRING)
saves enums as readable strings
The ShipmentStatus
enum represents fixed states:
public enum ShipmentStatus { PENDING, IN_TRANSIT, DELIVERED, CANCELLED }
And Priority
(another enum) tracks urgency levels.
Lombok annotations like @Getter
, @Setter
, @Builder
, etc. reduce boilerplate code.
🗂️ Repository Layer: Data Access
This layer connects our code with the database:
@Repository public interface ShipmentRepository extends JpaRepository<Shipment, Long> { Page<Shipment> findByStatus(ShipmentStatus status, Pageable pageable); Page<Shipment> findByOriginIgnoreCase(String origin, Pageable pageable); Page<Shipment> findByOriginIgnoreCaseAndStatus(String origin, ShipmentStatus status, Pageable pageable); Page<Shipment> findAll(Pageable pageable); long countByStatus(ShipmentStatus status); @Query("SELECT s.origin FROM Shipment s GROUP BY s.origin ORDER BY COUNT(s) DESC LIMIT 1") String findMostCommonOrigin(); }
Key Concepts:
- Spring Data JPA uses method names to auto-generate SQL
- Pagination is supported using
Pageable
— allowing APIs like:GET /api/shipments?page=0&size=10
orGET /api/shipments/search?origin=NY&status=IN_TRANSIT&page=1&size=5
- The custom
@Query
uses JPQL to find the most common origin efficiently
This structure ensures scalability and efficiency.
🌐 Controller Layer: API Endpoints
Here's a simplified controller class:
@RestController @RequestMapping("/api/shipments") public class ShipmentController { @Autowired private ShipmentRepository shipmentRepository; @PostMapping public Shipment createShipment(@RequestBody Shipment shipment) { shipment.setLastUpdatedTime(LocalDateTime.now()); return shipmentRepository.save(shipment); } @GetMapping public List<Shipment> getAllShipments() { return shipmentRepository.findAll(); } @GetMapping("/search") public List<Shipment> searchShipments( @RequestParam(required = false) String origin, @RequestParam(required = false) ShipmentStatus status, Pageable pageable ) { if (origin != null && status != null) { return shipmentRepository.findByOriginIgnoreCaseAndStatus(origin, status, pageable); } else if (origin != null) { return shipmentRepository.findByOriginIgnoreCase(origin, pageable); } else if (status != null) { return shipmentRepository.findByStatus(status, pageable); } else { return shipmentRepository.findAll(pageable); } } // more endpoints... }
Controller Annotations Explained:
-
@RestController
: Shortcut for@Controller + @ResponseBody
, It automatically converts your return values (like Java objects) into JSON responses -
@RequestMapping
: Base route for the class -
@Autowired
: Dependency injection of the repository -
@PostMapping
,@GetMapping
: Maps HTTP methods to Java methods -
@RequestParam
: Handles query strings like?origin=NY
-
@RequestBody
: Converts incoming JSON into Java objects
This is where HTTP meets business logic.
📊 Stats & DTOs
To keep response structure clean and decoupled from the database model, I used a DTO:
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class ShipmentStatsDTO { private long totalShipments; private Map<ShipmentStatus, Long> statusCounts; private String mostCommonOrigin; }
Why DTOs?
- Keeps internal model logic separate from external API responses — this is essential for security and flexibility
- Prevents overexposing database fields, such as sensitive IDs or internal flags
- Makes testing and documentation easier, since DTOs define exactly what data is expected or returned
- Helps decouple frontend and backend development — frontend teams can rely on stable response shapes even if internals change
- Promotes consistency in API design by enforcing contract-based responses
- Improves serialization performance and clarity by shaping only the needed data (e.g., filtering fields, renaming keys)
- Makes integration with tools like Swagger/OpenAPI or frontend types (like TypeScript interfaces) cleaner and easier to auto-generate
- Aids in unit testing: you can test service logic using DTOs directly without loading full entities
- Especially useful for read-heavy APIs or summary/analytics endpoints where the result doesn't mirror a database table model logic separate from API responses
- Prevents overexposing database fields
- Simplifies and formats responses (like maps and summaries)
The /api/shipments/stats
endpoint returns this structure.
🔧 Configuration & Secrets Management
To avoid committing passwords:
- Gitignored
application.properties
- Used
application.properties.example
for public template
🧳 Directory Structure Summary
freight-tracker/ ├── model/ # Java classes like Shipment, ShipmentStatus, Priority ├── repository/ # Interfaces for DB queries (Spring Data JPA) ├── controller/ # RESTful HTTP endpoints ├── dto/ # DTOs for stats and summaries ├── FreightTrackerApplication.java # Main entry point ├── application.properties.local
🧠 What I Learned in Phase 1
- How MVC architecture connects models, repos, and controllers
- How to build clean REST APIs using Spring Boot annotations
- How enums and timestamps are stored using JPA
- Query filtering using
@RequestParam
- Creating DTOs to shape and return custom responses
- Adding pagination to repository methods for real-world scalability
- Structuring code professionally (modularity, separation of concerns)
- Secrets management using
.gitignore
- Endpoints tested with Postman
🗓️ What’s Coming Next
- Real-time updates with WebSockets
- Service layer refactor
- UI or dashboard (React or Thymeleaf)
- AWS RDS and cloud deployment
👩💻 Final Thoughts
You can check out the code on GitHub [https://github.com/jayanti-neu/freight-tracker], and stay tuned for more updates!
If you have questions or feedback, feel free to connect with me. 🚀
Top comments (0)