An archived e-commerce management system — a Next.js admin dashboard backed by a Go API, handling products, orders, inventory, and background jobs. Archived because the client scope changed, but the architecture decisions are worth documenting.
What It Was
A private-label e-commerce back office for a small retail operation. The frontend was a Next.js admin dashboard — inventory management, order processing, supplier management, and reporting. The backend was a Go API chosen specifically for the high-throughput inventory update requirements and background processing of order fulfilment jobs.
Why Go for the Backend
The inventory system needed to handle concurrent stock updates without race conditions. When multiple orders arrive simultaneously for the same SKU, stock must be decremented atomically — classic concurrency problem. Go's approach to this, using database-level transactions with SELECT FOR UPDATE, is clean and explicit:
func (r *InventoryRepo) DecrementStock(
ctx context.Context,
tx *sql.Tx,
skuID string,
quantity int,
) error {
var currentStock int
err := tx.QueryRowContext(ctx,
"SELECT stock FROM inventory WHERE sku_id = $1 FOR UPDATE",
skuID,
).Scan(¤tStock)
if err != nil {
return err
}
if currentStock < quantity {
return ErrInsufficientStock
}
_, err = tx.ExecContext(ctx,
"UPDATE inventory SET stock = stock - $1 WHERE sku_id = $2",
quantity, skuID,
)
return err
}The SELECT FOR UPDATE acquires a row-level lock for the transaction duration. Concurrent updates to the same SKU serialise automatically. No application-level locks, no Redis-based distributed locks — just the database doing what databases are good at.
Background Job Processing
Order fulfilment triggered a chain of background tasks: stock reservation, supplier notification (if a SKU needed reordering), shipping label generation, and confirmation email. Go's goroutines with a worker pool made the job processor straightforward to reason about:
type JobProcessor struct {
workers int
queue chan Job
handlers map[JobType]HandlerFunc
}
func (p *JobProcessor) Run(ctx context.Context) {
for i := 0; i < p.workers; i++ {
go p.worker(ctx)
}
}
func (p *JobProcessor) worker(ctx context.Context) {
for {
select {
case job := <-p.queue:
p.process(job)
case <-ctx.Done():
return
}
}
}The Next.js Dashboard
The admin dashboard used Server Components for the heavy data-fetching pages (order lists, inventory tables) and Client Components only where interactivity was needed (inline editing, bulk action menus). This kept the bundle size down and made page loads feel fast even with large data sets.
The reporting module used a combination of PostgreSQL window functions and a charting library on the frontend. Revenue trends, bestseller lists, and inventory turnover were computed in a single query each rather than multiple round-trips.
Why It Was Archived
The client decided to pivot to an existing SaaS platform rather than continue with a custom build. This is a legitimate business decision — the development cost of a custom system is justified only when the requirements diverge significantly from what off-the-shelf tools provide. In this case, the requirements converged.
The codebase is archived rather than deleted because the inventory concurrency patterns and the job processor implementation are worth keeping as reference.
What I Took Away
The database-level concurrency approach — using SELECT FOR UPDATE rather than application locks — is something I now reach for immediately in any situation involving shared mutable state. It is simpler, more reliable, and removes an entire category of distributed systems complexity.
Go's explicit error handling, which feels verbose coming from TypeScript, becomes an asset when building something like a job processor where knowing exactly where failures occur and why matters enormously.