An open OEE engine: building ShopFloor API next to a production MES

At Kingsley Beverage in Dubai I am the sole developer of the MES/ERP platform that runs the plant's daily reporting. That system is private - it carries real production data and will stay behind the company's walls. Which creates a familiar portfolio problem: the work I am most qualified to talk about is the work I cannot show.

ShopFloor API is the answer: the same manufacturing domain - lines, job orders, downtime, OEE - rebuilt from scratch in the open, as a Spring Boot 3 / Java 21 backend with PostgreSQL, Flyway, JWT roles and a live Swagger UI. Not a copy of the production code (none of it is), but the same problems, solved where anyone can read the solution.

1. OEE in one pure class

Overall Equipment Effectiveness is the manufacturing KPI: Availability x Performance x Quality. Each factor looks trivial until real shift data arrives. The whole calculation lives in one dependency-free component, which makes it trivially unit-testable:

service/OeeCalculator.java

int totalUnits = goodUnits + rejectUnits;
int runTimeMinutes = Math.max(0, plannedRuntimeMinutes - downtimeMinutes);

BigDecimal availability = ratio(
        BigDecimal.valueOf(runTimeMinutes),
        BigDecimal.valueOf(plannedRuntimeMinutes));

BigDecimal idealMinutes = ratedUnitsPerHour <= 0
        ? BigDecimal.ZERO
        : BigDecimal.valueOf((long) totalUnits * 60)
                .divide(BigDecimal.valueOf(ratedUnitsPerHour), WORKING_SCALE, RoundingMode.HALF_UP);
BigDecimal performance = ratio(idealMinutes, BigDecimal.valueOf(runTimeMinutes));

BigDecimal quality = ratio(
        BigDecimal.valueOf(goodUnits),
        BigDecimal.valueOf(totalUnits));

The interesting part is what the helper refuses to do. ratio() guards divide-by-zero (a shift with zero planned minutes, a batch with zero units) and clamps every factor to [0, 1]. The clamp matters because real lines sometimes run above their rated speed - operators push a machine past nameplate - and a naive Performance of 1.07 multiplies through to an OEE over 100%, which destroys the metric's credibility with the people it is supposed to convince. BigDecimal with a fixed working scale, not double, because these numbers end up on reports that get compared line-by-line; nobody should ever have to explain a floating-point wobble in a management meeting.

2. Compute at the moment of truth, not on a timer

OEE is calculated when a job order closes - a state-machine transition (PLANNED → RUNNING → COMPLETED) - rather than by a scheduled job that sweeps the table. The production system taught me that: cron-style recomputation means a report pulled at 7:00 and one pulled at 7:20 can disagree, and the factory's trust in the number dies right there. Computing once, at close, from final inputs, makes the figure stable and auditable - the same event-driven shape the private MES uses.

3. A schema that cannot drift

Every table is created by a versioned Flyway migration, and Hibernate runs in validate mode - it checks the entity mapping against the migrated schema and refuses to boot on a mismatch, instead of silently "fixing" the database with ddl-auto. On a one-developer project this discipline looks like overhead until the first time it catches a column you renamed in the entity but not the migration. It boots clean on a fresh PostgreSQL every CI run, which is itself the test.

4. Tests that earn the green badge

The CI badge is only worth what runs under it: JUnit 5 unit tests on the calculator (the zero-division, over-speed and rounding cases each have one), MockMvc tests on the controllers and JWT role rules, and Testcontainers integration tests that start a real PostgreSQL in Docker - so the SQL, the migrations and the transaction boundaries are exercised against the same engine production would use, not H2's approximation of it.

5. What the factory version taught the open version

  • Downtime needs reason codes from day one. A single "downtime minutes" number answers "how bad"; only categorised reasons answer "what do we fix first".
  • Roles are a domain concept, not a security afterthought. An operator logs production; a supervisor approves it; a manager reads it. The JWT roles mirror the shop-floor hierarchy because that is what makes the data trustworthy.
  • FIFO inventory is harder than it looks. Consuming raw material against job orders in arrival order, with partial lots, is where spreadsheet logic quietly breaks - and exactly the kind of invariant a relational schema with constraints should own.

The repo is live - code, migrations, tests and the Swagger playground - at github.com/saad-mughal435/shopfloor-api. If you want to see the production story it shadows, the MES/ERP walkthrough shows the shape of the real platform without the real data.