Shop Management Application

Concept of shop management application for the printing apparel industry. A side effect of a patchwork of smaller efforts stemming from launching a past side commercial in this space.


Overview

Enterprise resource planning (ERP) systems are among the most complex and consequential software projects an engineering team can take on. They touch virtually every operational dimension of a business — inventory, procurement, finance, HR, manufacturing, and customer relations — and they must do so reliably, at scale, often under strict regulatory requirements.

This article documents the architecture decisions, engineering tradeoffs, and lessons learned from building a greenfield ERP platform from the ground up.

Domain Modelling

Before writing a single line of code the team spent several weeks in event-storming sessions with domain experts. The goal was to identify the core bounded contexts and their relationships:

  • Inventory — stock levels, warehouse locations, lot tracking
  • Procurement — purchase orders, vendor management, goods receipt
  • Finance — general ledger, accounts payable/receivable, cost centres
  • Production — work orders, bill of materials, routing steps
  • Sales — quotations, customer orders, dispatch and invoicing

Each context was assigned a dedicated service team and a clearly defined integration contract expressed as domain events published to a shared message bus.

Technology Stack

The backend is built on Java 21 with Spring Boot 3 and Spring Data JPA. We chose Spring for its mature ecosystem, excellent testing support, and first-class integration with Kafka for event streaming.

The API layer follows a REST-first approach for CRUD operations and a GraphQL facade for complex read queries that span multiple bounded contexts — the kind of query a dashboard widget or a reporting engine generates.

Node.js powers several lightweight integration adapters that translate between third-party EDI formats and our internal domain events. Its non-blocking I/O model is well suited to the high-throughput, low-compute nature of format translation.

Database Strategy

Each bounded context owns its own PostgreSQL schema, enforced through separate connection pools and schema-level permissions. Cross-context joins are prohibited at the database layer; consistency is achieved through eventual consistency via domain events.

A dedicated read model service subscribes to domain events and materialises denormalised projections optimised for reporting. This keeps the transactional write path lean while giving the reporting layer exactly the shape of data it needs.

API Design

We adopted a strict API-first workflow using OpenAPI 3.1. Contract files live in version control alongside the service code, and a CI gate compares each pull request against the published contract to catch breaking changes before they reach main.

Authentication uses short-lived JWT tokens issued by a centralised identity service. Authorisation is enforced at the service level using Spring Security with a custom @PreAuthorize expression that evaluates user roles and organisational unit membership.

Testing Approach

The test pyramid is taken seriously here:

  1. Unit tests cover domain logic in isolation with no infrastructure dependencies.
  2. Integration tests spin up an embedded PostgreSQL instance (via Testcontainers) and verify repository and service behaviour end-to-end within a single service boundary.
  3. Contract tests (Pact) verify that producers and consumers agree on the shape of domain events before any service is deployed to a shared environment.
  4. System tests run against a dedicated staging environment using synthetic data and cover the critical user journeys that span multiple services.

Deployment

Services are containerised with Docker and deployed to Kubernetes. Helm charts manage environment-specific configuration, and ArgoCD provides the GitOps-style continuous delivery pipeline.

Observability is built in from day one: structured JSON logs ship to OpenSearch, metrics are collected by Prometheus and visualised in Grafana, and distributed traces are captured via OpenTelemetry and stored in Tempo.

Lessons Learned

Start with the domain, not the framework. The weeks spent in event-storming paid for themselves many times over by keeping service boundaries aligned with real business concepts rather than technical convenience.

Eventual consistency is a first-class design constraint, not an afterthought. Every feature design must answer the question: what does the user experience look like in the window between a command being accepted and the read model being updated?

Contract testing at the event boundary is non-negotiable. Without it, a schema change in one service silently breaks consumers in ways that surface only in production.

Operational readiness is a feature. Dashboards, runbooks, and alerting rules should be delivered alongside the code that generates the signals they monitor.