A supply-chain parcel tracking REST API built with Java 17, Spring Boot 3, MongoDB, RabbitMQ, and JWT authentication. Designed to demonstrate production-grade backend architecture: async messaging, secure APIs, containerised deployment, and a clean layered codebase.
Client
│
▼
ParcelController ──► ParcelService ──► ParcelRepository (MongoDB)
│
▼
StatusUpdatePublisher
│
▼
RabbitMQ Exchange ( or Kafka )
│
▼
StatusEventConsumer
│
▼
StatusEventRepository (MongoDB)
Flow: When a parcel's status is updated, the service persists the new status to MongoDB and publishes a StatusUpdateEvent to RabbitMQ. The consumer picks up the event asynchronously and writes a full audit trail to the status_events collection. This decouples the write path from the audit/notification path — the same pattern used in warehouse and logistics systems.
| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 4.0, Spring MVC, Spring Data, Spring Security |
| Database | MongoDB |
| Messaging | RabbitMQ (AMQP), Kafka |
| Auth | JWT (jjwt 0.12.3), BCrypt |
| Build | Maven |
| Infra | Docker, Docker Compose |
Prerequisites: Docker Desktop installed and running.
# Clone the repo
git clone URI
cd tracker
# Start MongoDB + RabbitMQ + app
docker-compose up --buildThe API is live at http://localhost:8080.
RabbitMQ management UI: http://localhost:15672 (guest / guest).
All parcel endpoints require a JWT bearer token. Register a user first, then use the token.
POST /api/auth/register
Content-Type: application/json
{
"username": "warehouse_ops",
"password": "securepass123"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"username": "warehouse_ops"
}POST /api/auth/login
Content-Type: application/json
{
"username": "warehouse_ops",
"password": "securepass123"
}
All requests below require the header:
Authorization: Bearer <your_token>
POST /api/parcels
{
"description": "Electronics batch — order #4821",
"origin": "Mumbai Warehouse",
"destination": "Distribution Centre"
}Response: 201 Created
{
"trackingNumber": "PT-A1B2-C3D4",
"description": "Electronics batch — order #4821",
"origin": "Mumbai Warehouse",
"destination": "Amsterdam Distribution Centre",
"status": "CREATED",
"createdAt": "2024-05-10T09:00:00",
"updatedAt": "2024-05-10T09:00:00"
}PUT /api/parcels/{trackingNumber}/status ( Kafka Variant - /api/kafka/parcels/{trackingNumber}/status )
{
"status": "IN_TRANSIT",
"message": "Departed Mumbai hub — en route to Dubai"
}Valid statuses (in order):
CREATED → IN_TRANSIT → OUT_FOR_DELIVERY → DELIVERED
└──────────────────→ FAILED
GET /api/parcels/{trackingNumber}/history ( Kafka Variant - /api/kafka/parcels/{trackingNumber}/history )
Returns the full audit trail of every status change, persisted via RabbitMQ consumer.
src/main/java/com/subramanians/parceltracker/
├── config/
│ ├── RabbitMQConfig.java # Exchange, queue, binding, JSON converter
│ └── SecurityConfig.java # JWT filter chain, BCrypt, stateless sessions
│ └── KafkaConfig.java
├── controller/
│ ├── AuthController.java # /api/auth/register, /api/auth/login
│ └── ParcelController.java # /api/parcels CRUD + status update
│ └── KafkaParcelController.java # /api/parcels CRUD + status update
├── dto/ # Request/response records (Java 17 records)
├── exception/
│ ├── ParcelNotFoundException.java
│ └── GlobalExceptionHandler.java # Unified JSON error responses
├── messaging/
│ ├── KafkaStatusUpdatePublisher.java # Publishes events to Kafka topics
│ ├── StatusUpdateEvent.java # The message payload
│ └── StatusUpdatePublisher.java # Publishes events to the Rabbit MQ exchange
├── model/
│ ├── Parcel.java # @Document — parcels collection
│ ├── ParcelStatus.java # Enum: CREATED, IN_TRANSIT, etc.
│ ├── StatusEvent.java # @Document — status_events collection
│ └── User.java # @Document — users collection
├── repository/ # MongoRepository interfaces
├── security/
│ ├── JwtUtil.java # Token generation and validation (jjwt 0.12)
│ ├── JwtAuthFilter.java # OncePerRequestFilter — validates bearer token
│ └── UserDetailsServiceImpl.java
└── service/
├── AuthService.java # Register + login logic
├── KafkaParcelService.java # Kafka Publisher and Persists to MongoDB
├── KafkaStatusEventConsumer.java # Kafka Listener - Persists to MongoDB
├── ParcelService.java # Core business logic, publishes RabbitMQ events
└── StatusEventConsumer.java # @RabbitListener - persists events to MongoDB
Why both RabbitMQ and Kafka? The two brokers serve different architectural patterns. RabbitMQ is push-based with smart routing (exchanges, bindings, dead-letter queues) — ideal for task distribution and targeted delivery. Kafka is pull-based with durable, ordered, replayable logs — ideal for event sourcing, audit trails, and high-throughput pipelines. This project exposes both as parallel flows so the messaging characteristics of each can be observed directly.
Why a shared StatusUpdateEvent payload?
Both brokers carry the same event structure — tracking number, previous status, new status, message, timestamp. This makes the two flows directly comparable and reflects real systems where the same domain event is published to multiple brokers for different consumer use cases.
Why KRaft mode for Kafka? KRaft removes the Zookeeper dependency, reducing the number of moving parts in the stack. It has been production-ready since Kafka 3.3 and is the default from Kafka 4.0 onwards.
Why MongoDB? Parcel documents have flexible schemas (future fields like dimensions, weight, carrier metadata) and the status history is a natural document collection. MongoDB's flexibility suits logistics data better than a rigid relational schema.
Why JWT over sessions? The service is stateless by design — critical for horizontal scaling behind a load balancer (e.g. Kubernetes). No shared session store is needed.
| Variable | Local default | Docker value | Description |
|---|---|---|---|
SPRING_DATA_MONGODB_URI |
mongodb://localhost:27017/parcel_tracker |
mongodb://mongo:27017/parcel_tracker |
MongoDB connection |
SPRING_RABBITMQ_HOST |
localhost |
rabbitmq |
RabbitMQ host |
SPRING_KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
kafka:9092 |
Kafka broker address |
APP_JWT_SECRET |
(see application.yml) | — | Base64-encoded 256-bit signing key |
Subramanian S — Java Backend Engineer
linkedin.com/in/subramanian-s-782a4a204