Case Study: The Architecture Behind HoneyBee ERP

HoneyBee ERP is a modular enterprise platform covering HR, inventory, payroll, and core business workflows — 10+ modules, real-time updates, and role-based access for every kind of user from warehouse staff to finance managers. I own its backend end to end at HoneyBee IoT. This case study covers the architectural decisions that shape it: why it's a modular monolith and not microservices, how the module boundaries and RBAC work, and how real-time events and caching keep it fast.

Why a modular monolith, not microservices

ERP is the textbook case against microservices for a small team. The modules are deeply relational: payroll reads attendance, attendance reads HR, inventory movements post to accounting. Split those into services and every business transaction becomes a distributed transaction. Instead, the platform is one deployable with hard internal boundaries:

Two stacks, one contract

The platform runs NestJS (TypeScript) alongside Symfony (PHP) — a historical reality rather than a design goal, and a common one in ERP systems that evolve over years. The rule that keeps it sane: the stacks talk only through versioned REST contracts and shared MySQL is treated as owned by exactly one side per table. New modules are built in NestJS; the Symfony side keeps serving mature modules that have no business reason to be rewritten. A rewrite-everything project would have parked feature delivery for months and delivered the same product.

RBAC that survives real organisations

ERP access control fails when it's modelled as a flat role list. Real companies need "HR officer for branch A who can approve leave but only view payroll." The model that handles this:

Every API route declares the permission it requires; a NestJS guard resolves the user's effective permissions (cached in Redis, invalidated on role change) and the check costs microseconds instead of a database round trip per request.

Real-time without polling

Approvals, stock movements, and attendance events need to show up on screens immediately — managers sit on dashboards all day. The flow: a module emits a domain event, the event lands on Redis pub/sub, and a WebSocket gateway fans it out to connected clients subscribed to the relevant channels (scoped by the same RBAC rules, so users only receive events they're allowed to see). Redis pub/sub rather than direct emit means WebSocket gateways stay stateless: any instance can serve any client, which keeps horizontal scaling and zero-downtime deploys trivial.

Caching: reports are the hot path

ERP read traffic is dominated by dashboards and reports that aggregate thousands of rows. Three layers keep MySQL calm:

Shipping it

Everything runs in Docker with CI/CD on every merge: build, test, migrate, and roll out with health-checked container swaps so deploys are zero-downtime. Database migrations are forward-only and expand-contract (add the new column, backfill, switch readers, drop the old one later) — in an ERP, "we'll just take downtime for the migration" is never an acceptable answer, because someone is always running payroll.

What I'd tell anyone building an ERP

Resist distribution until a module's load forces it. Spend your complexity budget on the things ERP actually lives or dies by: airtight RBAC, an audit trail for everything, and reports that come back fast. The architecture above isn't novel — that's the point. Novelty is risk, and this system runs businesses.

Frequently asked questions

Should an ERP system use microservices?

Usually not. ERP modules are deeply relational (payroll reads attendance, inventory posts to accounting), so splitting them into services turns every business transaction into a distributed transaction. A modular monolith with hard internal boundaries gives the maintainability benefits without the distributed-systems tax, and leaves extraction open for later.

How do you design RBAC for an ERP?

Three layers: atomic per-module permissions (like payroll.run.execute), roles as configurable bundles of permissions, and scopes that bind a role to an organisational slice such as a branch or department. Every API route declares its required permission and a guard checks the user's effective permissions from a Redis cache.

How does HoneyBee ERP deliver real-time updates?

Modules emit domain events to Redis pub/sub, and a stateless WebSocket gateway fans them out to subscribed clients, filtered by the same RBAC rules. Using Redis pub/sub instead of direct emits means any gateway instance can serve any client, keeping scaling and zero-downtime deploys simple.

← All posts