Skip to content

Repository files navigation

Lainer

Shipment tracking service. Manages the lifecycle of delivery orders from creation to completion.

How Delivery Works

Imagine: a customer orders shipment delivery from point A to point B. What happens next?

Customer creates order
        ↓
   [PENDING] ← order waiting for driver assignment
        ↓
Dispatcher assigns driver
        ↓
   [ASSIGNED] ← driver heading to pickup
        ↓
Driver picked up shipment
        ↓
  [PICKED_UP] → [IN_TRANSIT] → [ARRIVED]
        ↓
Driver at destination, waiting for customer
        ↓
   [DELIVERED] ← shipment delivered, awaiting confirmation
        ↓
    ┌───┴───┐
    ↓       ↓
[COMPLETED] [RETURNED]
  customer   customer didn't
  picked up  pick up within 72h

At any point before final status, order can be cancelled → CANCELLED.

Why this flow? In real logistics, shipment physically moves through space. You can't skip stages. You can't deliver what wasn't picked up. You can't complete an order until customer confirms receipt.

What's Stored in an Order

  • Reference number — human-readable ID (SHP-20260319-A7B3C2)
  • Origin/Destination — pickup and delivery addresses
  • Driver and vehicle — assigned by dispatcher
  • Cost — what customer pays and what driver earns
  • Change history — who changed what and when

Architecture

Why Clean Architecture

Business logic shouldn't depend on how we store data or which protocol we use. Tomorrow we might want to swap MongoDB for PostgreSQL — we only change the storage layer, service keeps working.

┌─────────────────────────────────────────────────┐
│                   Transport                      │
│                    (gRPC)                        │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│                   Service                        │
│           (orchestration, transactions)          │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│                   Domain                         │
│    (Shipment entity, status transitions,         │
│     validation, history tracking)                │
└─────────────────────┬───────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│                  Storage                         │
│            (MongoDB, Redis cache)                │
└─────────────────────────────────────────────────┘

Domain — the core. This is where the Shipment entity lives with all status transition logic. It knows nothing about MongoDB or gRPC.

Service — orchestration. Receives "update status" command, fetches shipment from DB, calls entity method, saves result, invalidates cache.

Transport — adapter. Transforms gRPC requests into service calls and back.

Transactional Outbox

When order status changes, we need to send an event to Kafka. But what if Kafka is down? We can't lose events.

Solution: save the event in the same transaction as the order change. Background worker periodically picks up unprocessed events and publishes to Kafka.

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Service    │────▶│   MongoDB    │────▶│   Worker     │────▶ Kafka
│              │     │  (shipment   │     │  (outbox     │
│  updateStatus│     │   + outbox)  │     │  processor)  │
└──────────────┘     └──────────────┘     └──────────────┘
                          │
                          └── single transaction

Caching

Read often, write rarely. Redis as first-level cache:

  1. GetShipment(id) → check Redis
  2. If found → return
  3. If not → fetch from MongoDB, store in Redis
  4. On any write → delete from Redis

Why delete instead of update? Simpler and more reliable. Cache invalidation is one of two hard problems in CS.

Auto-Return

If customer doesn't pick up shipment within 72 hours — order automatically transitions to RETURNED. Background worker checks delivered_at and marks expired ones.

What Can Be Improved

Idempotency. Currently, duplicate status change request fails if status already changed. Real system needs idempotency keys.

Multi-tenancy. Service is single-tenant. SaaS would need data isolation by organization.

Saga pattern. If dependent services appear (billing, notifications), we'd need distributed transaction coordination.


Running

# Infrastructure
docker compose up -d

# Service
go run ./cmd/app

# Tests
make test            # unit
make integration     # integration (spins up Docker)

Configuration

Variable Default Description
GRPC_PORT 50051 gRPC server port
MONGODB_URI mongodb://localhost:27017 MongoDB
REDIS_ADDR localhost:6379 Redis
KAFKA_BROKERS localhost:9092 Kafka
SHIPMENT_PICKUP_TIMEOUT 72h Pickup timeout

Stack

  • Go 1.24
  • gRPC + Protocol Buffers
  • MongoDB (replica set for transactions)
  • Redis (cache)
  • Kafka (events)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages