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)