September 1, 2026 · 10 min read
An recent effort in the web analytics & application performance management space. Pushing my technical limits, formalizg some long overdue thoughts on room for improvement in this space.
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.
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:
Each context was assigned a dedicated service team and a clearly defined integration contract expressed as domain events published to a shared message bus.
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.
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.
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.
The test pyramid is taken seriously here:
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.
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.