The Challenge
Pharmacies frequently struggle to locate distributors with available stock for essential medicines. Distributors export inventory data in varying formats (.CSV, .PDF) with no centralized, searchable catalog.
Case Study · B2B Infrastructure
B2B pharmaceutical stock management platform connecting Stockists (distributors) and Retailers (pharmacies). Stockists upload inventory files; the system processes them into a searchable catalog so retailers can instantly discover which distributors carry the medicines they need.
Overview
A feature-first modular monolith built in Go to streamline B2B pharmaceutical stock discovery without microservice overhead.
Pharmacies frequently struggle to locate distributors with available stock for essential medicines. Distributors export inventory data in varying formats (.CSV, .PDF) with no centralized, searchable catalog.
Distributors upload inventory files to the platform. An automated background worker parses, validates, and seeds a global medicine catalog in PostgreSQL, linking availability to specific stockists in real-time.
Data Path
End-to-end flow from distributor inventory upload to pharmacy lookup.
flowchart LR
subgraph Actors["Actors"]
STK["Stockist"]
RTL["Retailer"]
ADM["Admin"]
end
subgraph System["PharmaStock Backend"]
API["API Server - Echo v5"]
UI["Browser UI - HTMX"]
W["Worker Service"]
DB[("PostgreSQL Database")]
end
STK -->|Upload CSV or PDF| API
API -->|Store file and job| DB
W -->|Poll pending jobs| DB
W -->|Parse and seed catalog| DB
RTL -->|Search medicines| UI
ADM -->|Manage users| UI
UI -->|Query catalog| API
API -->|Return results| DB
Stack
| Layer | Technology | Implementation Details |
|---|---|---|
| Runtime & Language | Go 1.24+ | High-performance compiled binary for API server and background job worker. |
| Web Framework | Echo v5 | Lightweight HTTP routing, middleware pipeline, and graceful server shutdown. |
| Database | PostgreSQL 13+ with pgx | Raw SQL with high-performance connection pooling (min 2, max 10 connections). |
| Frontend UI | Go HTML Templates + HTMX + Alpine.js | Dynamic server-rendered UI without heavy JavaScript SPA frameworks. |
| Authentication | JWT (HS256) & bcrypt | Stateless authentication with fine-grained Role-Based Access Control (RBAC). |
| Catalog Search | PostgreSQL pg_trgm GIN Index | Sub-20ms fuzzy matching on medicine names using trigram similarity. |
| Deployment & Infra | Linux VPS + Nginx Reverse Proxy | Nginx for TLS termination, proxying to Systemd-managed Go binaries & Dockerized DB. |
Architecture
Every feature domain (Auth, Stockist, Retailer, Medicine, Inventory, Upload, Job) is structured as a self-contained module with clean dependency injection.
flowchart TB
subgraph Server["Echo v5 API Server"]
Router["Router Engine"]
MW["Middleware Stack"]
subgraph Modules["Feature Modules"]
Auth["auth module"]
Stockist["stockist module"]
Retailer["retailer module"]
Medicine["medicine module"]
Inventory["inventory module"]
Upload["upload module"]
Job["job module"]
UI["ui module"]
end
end
subgraph Worker["Background Worker"]
W["Job Processor"]
end
subgraph Storage["Storage Layer"]
PG[("PostgreSQL Database")]
FS["File Storage"]
end
Router --> MW
MW --> Modules
Upload --> FS
Modules --> PG
W --> PG
W --> FS
Every domain directory maintains standard separation of concerns:
model.go — Pure Go domain entities (no JSON tags).dto.go — Request/response validation schemas.repository.go — pgx SQL query implementation.service.go — Core business rules & domain logic.handler.go & routes.go — HTTP handlers & routes.module.go — Wire constructor for DI initialization.stockist_id, medicine_id).Async Operations
Heavy CSV and PDF inventory parsing runs asynchronously to prevent HTTP request timeouts and keep the UI responsive.
stateDiagram-v2
[*] --> Pending
Pending --> Processing : Worker claims job
Processing --> Completed : File parsed and catalog seeded
Processing --> Failed : Invalid format or parse error
Processing --> Pending : Stale job reset timeout
Completed --> [*]
Failed --> [*]
When a stockist uploads an inventory file, the API saves the raw file to disk and inserts a job record with status pending.
The background worker polls PostgreSQL every 10 seconds, picks up pending jobs, parses rows in memory, batch-inserts medicines using ON CONFLICT DO NOTHING, and links inventory records in bulk.
If a worker instance crashes mid-processing, jobs won't remain stuck indefinitely.
The worker executes a ResetStaleJobs() routine on every cycle, resetting any job stuck in processing for more than 5 minutes back to pending for automatic retry.
Production Infra
Deployed on a dedicated Linux VPS with an Nginx reverse proxy managing SSL/TLS termination, request forwarding, and process management via Systemd.
flowchart TB
Client["Client Browsers and API Clients"] -->|HTTPS Port 443| Nginx["Nginx Reverse Proxy"]
subgraph VPS["Linux VPS Server Environment"]
Nginx -->|Proxy Pass Port 8080| API["Go API Server - cmd/api"]
Worker["Background Worker - cmd/worker"]
API -->|Save or Read| FS["File Storage - uploads"]
Worker -->|Parse Files| FS
subgraph Docker["Docker Containers"]
PG[("PostgreSQL 13 Database")]
end
API -->|Port 5432| PG
Worker -->|Port 5432| PG
end
X-Request-ID header propagation.golang-migrate tool before service spin-up.SIGINT/SIGTERM and allow 10s for active HTTP requests to complete before closing DB pools.Engineering
End-to-end backend engineering and system architecture designed and implemented by Dhrutinandan Swain.
pg_trgm GIN trigram indexing.ON CONFLICT DO NOTHING).Trade-Offs
| Decision | Rationale & Trade-Off |
|---|---|
| Modular Monolith Architecture | Ensures strict domain boundaries while eliminating microservice network overhead, deployment complexity, and distributed tracing costs. |
| Raw SQL (pgx) over ORM | Provides 100% control over query performance, eliminates hidden N+1 queries, and leverages native PostgreSQL features like trigram indexing. |
| DB Polling Worker over External Queue | Avoids adding Redis or RabbitMQ infrastructure complexity for moderate file upload volumes, utilizing stale-job reset routines for recovery. |
| HTMX + Alpine.js over Heavy SPA | Delivers fast, server-rendered interactivity without complex node build pipelines, heavy JavaScript bundles, or hydration mismatches. |
| Per-Page Template Clones | Pre-compiles HTML template sets per page route, preventing block definition collisions in Go's html/template engine at runtime. |
| Idempotent Ingestion Operations | Uses ON CONFLICT DO NOTHING so distributors can re-upload updated inventory files safely without corrupting catalog integrity. |