DEV Community

jayanti-neu
jayanti-neu

Posted on

Building a Real-Time Freight Tracker in Java — Beginner’s Perspective

🚚 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; } 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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(); } 
Enter fullscreen mode Exit fullscreen mode

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 or GET /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... } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

🧠 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)