Spring Boot 3.3 / Java 21 backend for Wassalha, a peer-to-peer shipping marketplace that connects shippers (people who need something delivered) with carriers (drivers who already travel that route). The platform handles dynamic ML-based pricing, real-time bidding, geographic dispatch, live tracking, in-app chat, payments, and a full admin portal.
- What it does
- Stack
- Architecture at a glance
- Project layout
- Modules
- User roles
- Domain model
- Order lifecycle
- Key flows
- REST API surface
- Real-time (WebSocket) endpoints
- External integrations
- Cross-cutting concerns
- Scheduled jobs
- Resilience
- Redis key reference
- API response shape
- Running locally
- Configuration
- API docs
A shipper opens the app, describes a package (item, dimensions, weight, pickup + drop address), and gets an ML-predicted price range in seconds — adjusted for distance, day of week, weather, and traffic zone. The order is published to nearby carriers (filtered by geographic zone in Redis). Carriers place bids; the shipper accepts one, an atomic claim removes the order from dispatch, and a live tracking session opens over WebSocket. Pickup and drop are confirmed with an OTP/QR. Payment is settled via wallet + Stripe, with built-in reconciliation. Throughout, a separate animal-image checker screens uploaded photos to refuse livestock shipments.
| Layer | Choice |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.3.0 |
| Build | Maven (wrapper included: mvnw / mvnw.cmd) |
| Persistence | Spring Data JPA + Hibernate 6.5 on PostgreSQL |
| Cache / coordination | Redis (Lettuce), ShedLock, Caffeine |
| HTTP client | Spring WebFlux WebClient (Reactor Netty) |
| Real-time | Spring WebSocket + STOMP (SockJS fallback) |
| Security | Spring Security + JWT, Google OAuth, BCrypt |
| Resilience | Resilience4j (circuit breakers, retries, timeouts) |
| Rate limiting | Bucket4j on Redis |
| Mapping | MapStruct |
| Validation | Jakarta Bean Validation |
| Docs | springdoc-openapi (Swagger UI) |
| Money | BigDecimal (scale 2, HALF_UP) |
| Container | Docker (multi-stage build, Alpine JRE 21) |
┌────────────────────────────────────────────────────────────────────────────┐
│ Mobile / Web client │
└──────────────┬───────────────────────────┬─────────────────────────────────┘
│ REST + JWT │ WebSocket (STOMP/SockJS)
▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ Spring Boot backend (this repo) │
│ │
│ Auth ─ Orders ─ Pricing ─ Dispatch ─ Bidding ─ Payment ─ Chat ─ Tracking │
│ │ │ │ │ │
│ │ │ │ └─ STOMP broker (driver pos.) │
│ │ │ └─ Redis (zones, claim locks, rate limits) │
│ │ └─ Pricing AI (FastAPI) + OpenWeather / Open-Meteo │
│ └─ Animal-checker AI (FastAPI, fail-closed) │
│ │
└────┬────────────────┬──────────────┬──────────────┬────────────────────────┘
▼ ▼ ▼ ▼
Postgres Stripe Cloudinary Firebase FCM
(payments) (images) (push)
- Pricing flow: order request → parallel weather + ML calls (
Mono.zip) → cached prediction → final price with adegradedflag if any upstream fails. - Dispatch flow: each order is bucketed into a bit-packed zone ID
(latZone << 32) | (lngZone & 0xFFFFFFFFL)and tracked in Redis sets; carriers query their zone(s). - Bidding flow: carriers POST offers; shipper accepts → a Redis Lua script atomically claims the order + removes it from dispatch.
- Image checks: any uploaded order image is screened by the animal-checker; the pipeline is fail-closed (upload rejected if the checker is unreachable).
src/
├── main/
│ ├── java/com/spring/g_p/
│ │ ├── GPApplication.java ← @SpringBootApplication
│ │ ├── auth/ ── registration, login, JWT, OTP, Google OAuth
│ │ ├── orders/ ── order lifecycle, events, status machine
│ │ ├── pricing/ ── ML-based dynamic pricing + weather
│ │ ├── dispatch/ ── zone-based carrier dispatch (Redis)
│ │ ├── bidding/ ── carrier offers & shipper acceptance
│ │ ├── payment/ ── wallet, Stripe, withdrawals, reconciliation
│ │ ├── notification/ ── Firebase Cloud Messaging
│ │ ├── inappchat/ ── WebSocket chat + conversations
│ │ ├── livetracking/ ── driver location via STOMP
│ │ ├── orderverification/ ── pickup / delivery OTP & QR confirm
│ │ ├── identityverification/── national-ID verification flows
│ │ ├── imagecheck/ ── animal detection (fail-closed)
│ │ ├── rating/ ── shipper ↔ carrier reviews
│ │ ├── profile/ ── profile, addresses, password
│ │ ├── admindashboard/ ── back-office (users, KYC, payments, support)
│ │ └── common/ ── security, JWT filter, exception handler,
│ │ rate limiting, auditing, image service, OpenAPI
│ └── resources/
│ └── application.properties
└── test/
| Package | Purpose |
|---|---|
auth |
Registration, login, JWT issue/refresh, OTP, Google OAuth, revoked tokens. |
orders |
Create / cancel / track orders, status transitions, event publishing. |
pricing |
Calls pricing ML + OpenWeather/Open-Meteo in parallel, caches predictions. |
dispatch |
Zone-bucketed availability in Redis, reconciliation against Postgres. |
bidding |
Carrier offer placement, shipper acceptance, atomic claim via Lua. |
payment |
Wallet balances, Stripe top-up & webhooks, withdrawal requests. |
notification |
Push notifications via Firebase, in-app notification feed. |
inappchat |
One-to-one chat over STOMP; persisted conversations & messages. |
livetracking |
Carrier location broadcast to shipper subscribers. |
orderverification |
Pickup/delivery confirmation via OTP or QR. |
identityverification |
KYC status, national-ID image handling. |
imagecheck |
Animal detection on uploaded images (rejects if AI down). |
rating |
Post-trip ratings + aggregate scores. |
profile |
Profile fields, address book, password change, account deletion. |
admindashboard |
Multi-controller back office: users, KYC, transactions, ratings, support, configs. |
common |
Shared infra (security, exceptions, rate limiting, auditing, image upload, OpenAPI). |
| Role | Granted on | What it can do |
|---|---|---|
| USER | Registration (default) | Create / cancel orders, place bids, chat, rate, manage wallet & profile. The same user can act as shipper, receiver, or carrier depending on the order context. |
| ADMIN | Provisioned manually | Full admin dashboard: users, KYC verification, transactions, ratings, notifications, message moderation, support tickets, global search, platform configs. All /api/v1/admin/** routes require hasAuthority('ADMIN'). |
Business rule enforced server-side: on any given order, the sender, receiver, and carrier must all be distinct users.
Authorization is enforced by @PreAuthorize on controllers + JWT claim validation in JWTAuthenticationFilter.
Core @Entity classes (all extend BaseEntity → id, @Version, audit fields, soft-delete):
| Entity | Key fields | Relationships |
|---|---|---|
| User | username, email, phone, password (BCrypt), role, verified, deleted | 1:1 → Wallet; 1:N → Order (as sender/receiver/carrier), Bid, Rating, Notification, ChatMessage |
| Order | itemName, description, weight, L/W/H, pickup/drop addr + lat/lng, estimatedPrice, lowPrice/normalPrice/highPrice, finalPrice, status, zoneId, pickupCode, dropCode, payment-state flags |
ManyToOne → sender, receiver, carrier (all User) |
| Bid | offerPrice (BigDecimal), status (PENDING / ACCEPTED / REJECTED / CANCELLED) | ManyToOne → order, carrier |
| Wallet | balance, availableBalance | OneToOne → user |
| WalletTransaction | amountCents, type (CREDIT / DEBIT / RESERVED), status, externalPaymentId | ManyToOne → wallet, order |
| PaymentRecord | externalPaymentId, status (PENDING / SUCCEEDED / FAILED / REFUNDED), webhookProcessed | ManyToOne → user |
| WithdrawalRequest | amountCents, destinationToken, externalPayoutId, status, otp | ManyToOne → user |
| Rating | stars (1–5), message; unique on (order_id, rater_id) | ManyToOne → order, rater, driver |
| NotificationEntry | title, message, type, referenceId, isRead | ManyToOne → user |
| Conversation | lastMessage, lastMessageTimestamp, chatType, unreadCountA/B | OneToOne → order; ManyToOne → participantA / B |
| ChatMessage | message, type (TEXT/…), timestamp | ManyToOne → conversation, sender, receiver |
| UserAddress | label, lat/lng, fields | ManyToOne → user |
| RevokedToken | token (indexed), expiryDate | standalone |
| SupportTicket, SystemConfig | admin & ops data | — |
┌─ DRAFT
│ │ shipper completes details + image check passes
│ ▼
│ AWAITING_RECEIVER_CONFIRMATION
│ │ receiver confirms
│ ▼
│ WAIT_FOR_DELIVERY ← published to dispatch (zone in Redis)
│ │ shipper accepts a bid ← atomic Redis claim removes from dispatch
│ ▼
│ MATCHED
│ │ carrier confirms pickup (OTP / QR)
│ ▼
│ IN_TRANSIT
│ │ carrier confirms delivery (OTP / QR)
│ ▼
│ DELIVERED ──────────────► payment settles + ratings opened
│
└────► CANCELLED (terminal, reachable from most states)
Status transitions are guarded inside OrderService and emit Spring application events that the dispatch, notification, and payment modules listen to.
register ──► sendOTP ──► verifyOTP ──► login ──► access + refresh JWT
│
└──► /refresh (X-Refresh-Token header)
└──► /logout (token → RevokedToken table)
JWT claims: USERNAME, ROLE (USER / ADMIN), TYPE (ACCESS / REFRESH), iss, exp.
| Token | Default lifetime |
|---|---|
| Access | 24 h (security.jwt.access-token-expiration) |
| Refresh | 7 d (security.jwt.refresh-token-expiration) |
- Refresh tokens are sent via the
X-Refresh-Tokenheader and validated against theTYPEclaim. - Logout writes the token into
revoked_tokens; the JWT filter rejects any token present there. - A nightly job purges expired revoked tokens at 03:00.
PricingService.getPrice(TripReqForPricing) ──┐
│
Mono.zip( pickupWeather, │ ─► OpenWeatherMap / Open-Meteo (CB)
dropWeather, │
aiPrediction ) │ ─► Pricing-AI POST /predict_price (CB + retry)
│
─► PricingResponse { │
lowPrice, │ BigDecimal, scale 2
normalPrice, │
highPrice, │
degraded: true|false │ true if AI or weather call failed
}- Cache: 30-min Caffeine cache on AI predictions keyed by request shape.
- Fallback heuristic when AI is down:
8 + 0.5×distanceKm + 10(if rain) + 10(if extreme temp) + size/weight uplift. - Price clamped to
[pricing.min-price, pricing.max-price]. - AI request is snake_case; DTO uses
@JsonPropertyper field.rain&day_of_weekare strings.
Zone math (GridService):
- Grid cell = 0.02° × 0.02° lat/lng.
zoneId = ((latZone << 32) | (lngZone & 0xFFFFFFFFL))— singlelong.- Carrier query expands to a square neighbourhood (configurable via
dispatch.grid.default-range).
Redis structures (ZoneRedisRepository):
| Key | Type | Purpose | TTL |
|---|---|---|---|
zone:{zoneId} |
Set | Order IDs in that zone | 25 min |
order:zone:{orderId} |
String | Reverse map: order → zone | — |
order:claim:{orderId} |
String | Atomic claim lock during bid acceptance | 30 s |
zone:index |
Set | All active zones (used by reconciliation) | — |
Atomic claim: Lua script CLAIM_AND_REMOVE_SCRIPT does SET NX + SREM in one round trip, preventing two shippers from accepting the same bid.
Reconciliation: RedisReconciliationJob (ShedLock-protected) periodically re-syncs Postgres active orders with Redis zones, healing drift.
- Top-up: client creates a
PaymentIntentvia Stripe; onpayment_intent.succeededwebhook the backend credits the wallet exactly once (idempotent viawebhookProcessedflag + dedup onexternalPaymentId). - Order settlement: carrier deposit + sender amount are reserved (
WalletTransactiontype=RESERVED) when the order is matched, and committed on delivery confirmation. - Withdrawal: user requests payout → OTP sent → on confirmation a
WithdrawalRequestis created;stripe.payout.mock=trueshort-circuits this in dev. - Reconciliation job:
ReconciliationJobsweeps pendingPaymentRecords and reconciles them against Stripe.
Stripe webhook events handled (/payments webhook):
| Event | Action |
|---|---|
payment_intent.succeeded |
mark PaymentRecord SUCCEEDED, credit wallet |
payment_intent.payment_failed |
mark PaymentRecord FAILED |
charge.failed |
mark PaymentRecord FAILED |
| (others) | logged at DEBUG, ignored |
Signature is verified with stripe.webhook.secret. Replays are idempotent.
Full interactive docs:
/swagger-ui.htmlonce the app is running.
| Base path | Module | Highlights |
|---|---|---|
/auth |
auth | POST /register, POST /login, POST /refresh, POST /verify-otp, POST /logout |
/orders |
orders | create, list, get, cancel, update-status, search |
/orders/carrier |
dispatch | GET /availableOrders (zone-filtered) |
/orders/verify |
orderverification | POST /verifyPickup, POST /verifyDelivery |
/pricing |
pricing | POST /predict |
/bids |
bidding | check availability, place / accept / reject offers |
/wallet |
payment | top-up, balance, status |
/payments |
payment | Stripe webhook |
/profile |
profile | profile, addresses, password, delete account |
/ratings |
rating | create, get, list |
/notifications |
notification | feed, unread count, mark as read |
/chat |
inappchat | REST list / get messages |
/track/{orderId} |
livetracking | current driver position |
/api/v1/admin/auth |
admin | login, refresh, logout |
/api/v1/admin/users |
admin | list / detail / updateStatus / bulkActions |
/api/v1/admin/verifications |
admin | list, approve, reject |
/api/v1/admin/dashboard |
admin | stats, orders-over-time, recent transactions |
/api/v1/admin/transactions |
admin | list, detail |
/api/v1/admin/ratings |
admin | list, detail |
/api/v1/admin/notifications |
admin | send, list |
/api/v1/admin/messages |
admin | search, block |
/api/v1/admin/support |
admin | list, detail, respond |
/api/v1/admin/search |
admin | global search |
/api/v1/admin/configs |
admin | manage platform settings |
STOMP over SockJS; clients connect with the JWT in the CONNECT frame.
| Destination | Direction | Purpose |
|---|---|---|
/app/chat.sendMessage |
client → server | Send a chat message in a conversation |
/user/queue/messages |
server → client | Receive chat messages (per-user queue) |
/app/track.updateLocation/{orderId} |
carrier → server | Push current GPS position |
/topic/track/{orderId} |
server → shipper | Broadcast carrier position |
/user/queue/errors |
server → client | WebSocket error channel (see GlobalExceptionHandler#handleChatErrors) |
| Service | Purpose | Resilience |
|---|---|---|
| PostgreSQL | Primary store | Hikari pool, optimistic locking via @Version |
| Redis | Dispatch zones, rate-limit buckets, ShedLock | Lettuce pool |
| Stripe | Card payments, webhooks, payout (mockable) | Webhook signature verification |
| Cloudinary | Order / profile / ID images | — |
| Firebase Admin SDK | Push notifications | — |
| Pricing AI (FastAPI) | ML price prediction | Resilience4j CB pricingAi, 5 s timeout, 2 retries, Caffeine cache |
| Animal-checker AI (FastAPI) | Reject animal images | Resilience4j CB animalChecker, fail-closed |
| OpenWeatherMap | Current weather | CB openWeather |
| Open-Meteo | Fallback weather | CB openMeteo |
Located under common/:
WebSecurityConfig— stateless JWT auth, CORS allow-list, method security via@PreAuthorize.JWTAuthenticationFilter— extracts & validates Bearer tokens once per request; consultsrevoked_tokens.GlobalExceptionHandler— maps domain exceptions to consistentApiResponsepayloads (404, 401, 403, 409, 422, 429, 500), including optimistic-lock conflicts, validation errors, and rate-limit429withRetry-After.@RateLimited+RateLimitInterceptor— Bucket4j-on-Redis annotation-driven throttling.BaseEntity—id,@Version, Hibernate@CreationTimestamp/@UpdateTimestamp, Spring@CreatedBy/@LastModifiedBy, soft-delete flag.AuditorAwareImpl— pulls current principal for auditing.ImageService— Cloudinary upload helper.FirebaseConfig— Admin SDK init fromclasspath:service-account JSON.OpenApiConfig— Swagger UI with JWT bearer auth.
| Job | Location | Cadence | Notes |
|---|---|---|---|
ReconciliationJob |
payment/scheduler |
@Scheduled |
Reconciles pending Stripe payments. |
RedisReconciliationJob |
dispatch/job |
@Scheduled + @SchedulerLock |
Re-syncs active orders into Redis zones. ShedLock prevents double-run. |
| Revoked-token cleanup | auth |
Daily 03:00 | Deletes tokens past their expiryDate. |
Every outbound integration is wrapped in a Resilience4j circuit breaker configured in application.properties:
| Instance | Used by |
|---|---|
pricingAi |
Pricing ML service |
animalChecker |
Animal image checker |
openWeather |
OpenWeatherMap |
openMeteo |
Open-Meteo (weather fallback) |
Shared defaults: sliding-window 20, minimum-calls 10, failure-rate threshold 50 %, open-state 30 s, half-open with 3 permitted calls, auto half-open. Overlaid with per-client timeouts (5–15 s) and retry filters that only retry transient network failures (never 4xx).
| Key pattern | Type | Purpose | TTL |
|---|---|---|---|
zone:{zoneId} |
Set | order IDs in zone | 25 min |
order:zone:{orderId} |
String | reverse zone lookup | — |
order:claim:{orderId} |
String | atomic accept-bid lock | 30 s |
zone:index |
Set | all currently populated zones | — |
zone:tmp:{...} |
Set | temp keys during batched ops | short |
rl:{policy}:{discriminator} |
Bucket4j | rate-limit token bucket | per policy |
withdrawal:req:{transactionId} |
String/Hash | OTP state for payout request | 60 min |
shedlock:{jobName} |
String | scheduled-job lease | per lock |
All non-streaming responses use ApiResponse<T>:
{
"message": "Order created successfully",
"data": {
"id": 142,
"status": "AWAITING_RECEIVER_CONFIRMATION"
}
}datais omitted whennull(@JsonInclude(NON_NULL)).- Errors share the same envelope; HTTP status code is the source of truth.
- Validation errors include a per-field
datamap:
{
"message": "Pickup zone is required, Distance must be positive",
"data": {
"pickupZone": "Pickup zone is required",
"distanceKm": "Distance must be positive"
}
}| HTTP | When |
|---|---|
200 / 201 |
success |
400 |
validation, illegal arg, notification general |
401 |
bad credentials, unverified user, bad JWT |
403 |
access denied (auth ok, perms missing) |
404 |
user / notification / resource not found |
409 |
duplicate user, optimistic-lock conflict, integrity violation |
422 |
animal detected in image |
429 |
rate limit exceeded (response has Retry-After) |
500 |
unexpected — response carries an error reference UUID also written to logs |
- JDK 21
- PostgreSQL (local or remote — point
spring.datasource.*at it) - Redis 7
# 1. Start Redis (one-time)
docker run -d --name g_p-redis -p 6379:6379 redis:7-alpine
# 2. Build
./mvnw clean package -DskipTests # Linux/macOS
.\mvnw.cmd clean package -DskipTests # Windows
# 3. Run
java -jar target/G_P-0.0.1-SNAPSHOT.jar
# or
./mvnw spring-boot:runThe server listens on http://localhost:8080. Swagger is at /swagger-ui.html.
docker build -t wassalha-backend:local .
docker run --rm -p 8080:8080 --network host wassalha-backend:localKey properties in src/main/resources/application.properties:
| Property | Controls |
|---|---|
spring.datasource.url / .username / .password |
Postgres connection |
spring.data.redis.host / .port |
Redis connection (defaults to localhost:6379) |
security.jwt.secret-key, *-token-expiration |
JWT signing & lifetimes |
google.client-id / .client-secret |
Google OAuth |
firebase.service-account-file |
Path to FCM admin key (classpath: or filesystem) |
cloudinary.cloud_name / .api_key / .api_secret |
Image uploads |
stripe.api-key / .webhook.secret / .payout.mock |
Stripe |
openweathermap.api.key |
OpenWeather |
pricing.ai.base-url |
Pricing ML service |
pricing.min-price / .max-price |
Final price clamp |
animal.checker.base-url / .api-key |
Animal-checker AI |
resilience4j.circuitbreaker.instances.* |
CB tuning per upstream |
dispatch.grid.default-range |
Zone neighbourhood radius |
dispatch.cache.zone-ttl |
TTL for cached zone entries |
⚠️ Production: load every credential from environment variables or--spring.config.location=...— do not ship the file with real secrets baked in.
- Swagger UI:
/swagger-ui.html - OpenAPI JSON:
/v3/api-docs
Both are alphabetically sorted, support "Try it out", and display request duration.