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 ×
Performance × Quality. Each factor looks trivial until real shift data arrives.
The whole calculation lives in one class with no injected collaborators - it is a Spring
@Component, but it takes five ints and returns four numbers, so it
is 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] - both bounds. 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.
Two honest caveats, because this is the paragraph that invites you to go and read the code.
The upper clamp is a defensible choice, not an obviously correct one. Standard OEE practice treats a Performance above 100% as a signal - it usually means the ideal cycle time or the unit count is misconfigured, not that the line beat physics. Silently clamping hides that. The version I would defend in a review is: clamp for display, keep the raw value, and raise a data-quality flag. The clamp is what ships today.
The divide-by-zero guard returns 0, and 0 is a real value. An unconfigured
nameplate speed drives Performance to zero and reports a confident OEE of 0%, which looks
identical to a catastrophic shift. That is the same failure I just argued against two
paragraphs up: a number the reader cannot distinguish from a real one. The right shape is an
explicit INSUFFICIENT_DATA rather than a zero, and the same goes for
Math.max(0, planned - downtime), which quietly swallows a data-entry error where
logged downtime exceeds planned runtime. Both are on the list.
BigDecimal with a fixed working scale, not double - though the
usual argument for it does not survive contact with this code. ratio() divides at
scale 8 and immediately rounds to 4, so each factor is rounded before the three are
multiplied, which introduces about 1e-4 of error - more than double would at
report precision. The real reason is not accuracy, it is determinism: the
rounding is specified, identical on every machine and every JVM, and reproducible when
somebody asks why last Tuesday's figure was 82.1% and not 82.2%. "Floating-point wobble" is
the wrong justification; auditability is the right one.
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.