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