Monesize Core: Engineering an Enterprise Finance & Operations Platform
Monesize Core did not begin as an attempt to build a large enterprise platform. It evolved.
The product started from a much smaller problem space. As the system grew, the limitations of the original approach became increasingly obvious. Business operations were becoming more complex, financial records were becoming more important, and the relationship between the two could no longer be treated as an afterthought.
That eventually led to a different architectural question: what if the operational system and the financial system were designed as parts of the same system from the beginning? That question became the foundation for Monesize Core.
Today, Core is a modular finance and operations platform built around a central accounting engine, operational domains, event-driven coordination, tenant isolation, compliance infrastructure, and a growing identity layer that allows independently deployed products to participate in the wider Monesize ecosystem.
This article documents how that architecture evolved, why particular decisions were made, and some of the engineering problems we ran into along the way.
The Problem We Were Actually Trying to Solve
Accounting software is very good at recording financial activity. Operational software is very good at managing operations. The problem begins when a business needs both.
A sale is not only an accounting event. It is also an operational event. A purchase is not only an expense. It can affect inventory, suppliers, branch operations, budgets, payables, and accounting. A payroll run is not simply a collection of employee payments. It also produces financial consequences. A stock transfer is an operational event that changes the state of inventory across locations.
The more complex the business becomes, the more difficult it becomes to keep these separate systems synchronized. This is one of the central ideas behind Core. I designed the platform around the principle that financial records should emerge naturally from operational activity rather than requiring finance teams to reconstruct them afterward.
The accounting module therefore sits underneath the operational platform as a formal financial layer. Business activity can be translated into journal entries through structured posting rules, while the resulting accounting records remain available for ledgers, trial balances, and financial statements. That sounds straightforward. It was not. Building the system required solving a much larger set of problems around domain boundaries, data consistency, event delivery, tenant isolation, compliance, identity, deployment, and performance.
The Experiment That Changed the Architecture
One of the most important architectural decisions came from an earlier product experiment. We found that businesses operating in the same industry and within the same mid-market segment could have surprisingly different workflows. They did not necessarily want the same features, nor did they operate their businesses in the same way.
That changed how I thought about the platform. Instead of building one standardized system and expecting every customer to adapt their operations to it, Core would allow each organization to have its own deployment with only the modules relevant to its workflows.
This led to two decisions that became fundamental to the architecture: single-tenant deployments and independently structured modules.
Single tenancy allowed each organization to have its own backend, database, environment, configuration, and selected modules. The module architecture then had to ensure that one module did not become dependent on the internal implementation of another. That is ultimately where the need for event-driven coordination came from.
Choosing a Modular Monolith
One of the earliest architectural decisions was also one of the most important. Core would be a modular monolith.
There is a tendency in modern backend engineering to interpret a large system as a reason to immediately introduce microservices. I did not want that. Core contains multiple domains, but domain separation does not automatically require process separation.
The platform has distinct areas for things such as accounting, sales, inventory, payroll, procurement, expenses, projects, assets, customers and vendors, analytics, compliance, branch management, and identity and access control. These domains have different responsibilities and should not become one enormous collection of tightly coupled code. At the same time, splitting every domain into an independent service would introduce another class of problems entirely.
Instead, the architecture keeps the domains separated internally while retaining a unified deployment model. The result is a system where domain boundaries exist in the codebase, but the operational simplicity of a single application is preserved. That distinction became increasingly important as the platform grew. I wasn't trying to eliminate modularity. I was trying to get modularity without paying the operational cost of distributed services before that cost was actually justified.
Why the Accounting Engine Became the Centre of the Platform
The accounting engine is arguably the most important architectural component in Core. An operational record by itself is not financial truth. The accounting layer gives that record a formal financial meaning: a debit, a credit, a journal entry, a ledger movement, an account balance, a change to financial statements.
Core uses double-entry accounting as the foundation for this process. The design separates accounting configuration from the operational modules that generate financial events. A chart of accounts defines the accounts available to the organization. Posting rules define how specific types of business events should affect those accounts. The accounting engine then translates relevant operational activity into journal entries. A business event, for example, can result in a configured debit account and credit account without the operational module having to implement the complete accounting process itself.
That separation matters. If accounting logic were embedded independently inside Sales, Expenses, Payroll, Inventory, and every other domain, the system would eventually accumulate several different interpretations of accounting. Instead, the accounting engine provides a central financial language for the rest of the platform. This is one of the architectural decisions that makes the platform more than a collection of business modules. The operational modules generate business activity. The accounting engine gives that activity financial structure.
Connecting Operations to Finance
The next problem was synchronization. Suppose an operational transaction succeeds. The accounting entry associated with that transaction also needs to be created. But what happens if the operational transaction commits successfully and the accounting operation fails?
That is where a naive event-driven implementation can become dangerous. I did not want the system to reach a state where the business believes something happened, but the accounting system does not know about it. That led me to the Transactional Outbox pattern.
The basic idea is that the business transaction and its corresponding event record are persisted as part of the same database transaction. Only after the transaction succeeds does the event become available for asynchronous processing. That creates a much stronger reliability boundary. Instead of committing a database transaction and then simply hoping an event gets sent afterward, the system persists the business state and the outbox event together, commits them as one unit, and only then moves into event processing, with retries and dead-letter recovery available if anything goes wrong downstream.
This architecture became an important part of Core's event-driven coordination. The implementation also includes automated retries and dead-letter recovery for failures that cannot be resolved through normal retry attempts. The objective was not to make the system distributed for the sake of being distributed. The objective was much simpler: do not lose important business events.
Events Instead of Direct Coupling Everywhere
The Outbox pattern solved one problem, but it also encouraged a broader architectural direction. Domains should not need to know everything about one another. If Sales creates a transaction that has accounting consequences, Sales should not necessarily need to understand every internal implementation detail of Accounting. Instead, a domain can produce an event describing something that happened, and other parts of the system can react to that event.
This creates a useful separation between what happened and what the system needs to do because it happened. That distinction became particularly valuable as the number of domains increased. It also allowed the platform to introduce asynchronous processing where appropriate without turning the entire application into a collection of asynchronous black boxes. This is where technologies such as Redis and BullMQ became useful for background jobs and workflow processing.
The important lesson for me was that event-driven architecture is not synonymous with Kafka, microservices, or distributed systems. It is fundamentally about how parts of a system communicate and how reliably the consequences of business events are processed.
Multi-Location Operations
Core was eventually designed around businesses where location matters. A branch is not simply a label attached to a transaction. It represents an operational context. Customers, vendors, inventory, procurement, expenses, payroll, and other records can have branch-level relevance, while the organization still needs consolidated visibility.
That creates two simultaneous requirements: local teams need appropriately scoped access, and management needs organization-wide visibility. This shaped the authorization model and the way operational records are scoped. The branch becomes part of the operational context rather than something added later for reporting purposes.
That distinction is particularly important for inventory. If an organization has several locations, inventory cannot simply be one global number. Stock exists somewhere. Transfers happen between locations. Managers need to understand what is available at their location while the organization needs a consolidated picture. The same principle applies to procurement and expenses. I treat organizational and branch context as architectural concerns rather than merely reporting metadata, and this approach is also reflected in the product's public positioning around multi-branch operations.
Tenant Isolation
As Core evolved, another architectural question became unavoidable: how should customer data be isolated? The answer was application-level tenant isolation. Every tenant-scoped operation must explicitly identify its tenant, and this is enforced through the application's data access layer rather than relying entirely on developers remembering to add tenant conditions manually.
The idea is straightforward: a tenant-scoped query should not be allowed to execute without a tenant scope. This is especially important in a large application because tenant isolation cannot depend solely on individual developers remembering a convention thousands of times. The architecture pushes tenant scoping closer to the database access layer, which provides a second line of defence against accidental cross-tenant access. It's one of those architectural decisions that is largely invisible when everything works correctly. That is exactly the point.
Security Became an Architectural Problem
As Core grew, authentication stopped being only a login problem. The platform eventually needed to support independent products. That changed the question from "how does a user authenticate with Core" to "how can another Monesize product know that a user and tenant were legitimately authenticated without receiving access to Core's authentication secrets or database?"
The answer became an identity trust layer. The system uses tenant-specific RSA key pairs and RS256-signed identity tokens. The architecture allows an independently deployed product to verify an identity cryptographically rather than trusting an opaque assertion from another application. This introduced another component into the ecosystem: the Gateway. The Gateway handles responsibilities including tenant public-key registration, issuer-based tenant resolution, JWT signature verification, and failure handling.
The important part is the trust model. A product does not need direct access to the Core database. It does not need Core's password secrets. It does not need to share its authentication implementation with Core. It can independently verify a token according to an established trust relationship.
From One Platform to an Ecosystem
This was one of the most significant architectural evolutions. Core was originally the product. Eventually, Core became something more fundamental, and independent products could now exist around it.
Monesize Desk is an example. Desk is independently deployed and has its own backend, database, application logic, and tenancy model, yet it can participate in the wider Monesize ecosystem. The relationship works by Core establishing identity trust that flows through the Gateway, which the independent product then relies on. The product can consume verified identity without being granted access to Core's database or authentication secrets.
This is an important distinction. The goal was not to turn Core into a giant shared backend for every future product. The goal was almost the opposite: products should be able to remain independent. The ecosystem should provide trust and interoperability without forcing every product into the same deployment boundary. That architectural decision eventually influenced how I designed Desk.
Building Desk Independently
Monesize Desk became an interesting test of whether the ecosystem architecture actually worked. Desk is a multi-tenant customer support and service management platform with its own authentication, organizations, tickets, teams, customers, knowledge base, attachments, analytics, notifications, customer portal, entitlement model, and tenant isolation.
It is not simply a module inside Core. It is an independent product. That distinction matters because the architecture now has to support communication between independent applications without collapsing them back into one application. Desk uses the identity bridge to establish trust with the broader ecosystem. It also introduced another class of infrastructure requirements, including real-time notifications through Socket.IO and email delivery through AWS SES. Building Desk became another test of the architectural decisions made around Core.
Real-Time Systems
Desk also introduced a problem Core did not need in the same form. Users need to see certain events as they happen. A ticket assignment can generate a notification. A new ticket message can generate a notification. An escalation can generate a notification. Waiting for the browser to repeatedly ask whether something changed is inefficient and produces a poor experience.
Desk therefore uses WebSockets through Socket.IO for real-time notification delivery. This reflects a broader architectural principle I keep coming back to: use the right communication model for the problem. Not every operation needs to be synchronous. Not every event needs a queue. Not every notification needs polling. Different workloads require different mechanisms.
Compliance as Part of the Architecture
Monesize Core also has to operate within real regulatory environments. For UK businesses, that includes Making Tax Digital for VAT. The HMRC integration is not treated as a separate reporting application. The system connects operational and financial data directly to the compliance workflow. VAT obligations can be retrieved, returns can be prepared from platform data, and submissions can be made through HMRC's APIs, with Fraud Prevention requirements also incorporated into the integration.
That matters architecturally because compliance depends on the integrity of the underlying records. If the financial data is fragmented, compliance becomes another reconciliation exercise. If the financial records already emerge from operational activity, compliance can consume that structured data instead. That is another reason the accounting engine sits so close to the centre of the platform.
Auditability
Financial systems need to answer more than "what is the current number." They need to answer "how did we get here." That requires history.
Core therefore treats auditability as part of the platform rather than an optional reporting feature. Business mutations need traceability. Financial entries need source references. Compliance activity needs to be attributable. The accounting layer retains journal history and source information, allowing financial activity to be traced back to its originating module and event. The accounting implementation also exposes journal history, ledger views, and trial balance functionality.
This becomes particularly valuable when debugging production issues. A system that can tell you what happened is useful. A system that can tell you why the current state exists is considerably more useful.
Performance Engineering
Architecture is not complete simply because the code is logically correct. At some point, you need to know what happens under load. I stress-tested one of the backend's heaviest endpoints using Autocannon. The test sustained more than 700 requests per second across 22,000 requests in 30 seconds, with zero request failures and approximately 54 percent CPU utilization on an eight-core AWS EC2 instance.
The significance of that test was not the number itself. A benchmark without context tells very little. What mattered was understanding how the application behaved under sustained load, where CPU utilization went, whether the database became the bottleneck, whether requests failed, whether latency degraded, and whether the application remained stable. Performance testing became another way of validating architectural decisions rather than simply producing a large requests-per-second number.
Deployment Is Part of the Architecture
A backend architecture does not end at the source code. Core runs in AWS infrastructure, with the production deployment involving Docker, PM2, Nginx, and SSL termination. That introduces another set of engineering concerns: process management, reverse proxying, TLS, environment configuration, application restarts, logging, resource utilization, deployment consistency, and production monitoring.
The deployment architecture has evolved alongside the application, and that evolution matters, because the infrastructure required to run a small backend comfortably is not necessarily the infrastructure required to operate a large enterprise platform. The same principle applies to the application itself. Architecture is not a static diagram. It changes as the system changes.
Why I Did Not Turn Core Into Microservices
This deserves its own section because it is probably one of the most misunderstood architectural decisions. Core has many domains. It has a large API surface. It has asynchronous processing. It has independent products around it. None of those facts automatically require Core itself to become a collection of microservices.
The platform is still a modular monolith. That was deliberate. There is a cost to every service boundary: deployment complexity, network communication, service discovery, distributed tracing, independent configuration, failure modes, versioning concerns, operational overhead, and more infrastructure to run and maintain. Those costs can be justified. But they should be justified by a real engineering requirement, not adopted by default.
For Core, internal modularity provided much of the separation I needed without immediately introducing those costs. The event-driven architecture provides asynchronous coordination where it's useful. The Gateway provides a boundary where independent products genuinely need one. The products themselves can be independently deployed when independence actually provides value. This produces a more deliberate architecture than simply declaring every domain a microservice by default.
What the Architecture Has Become
The interesting thing about Core today is that it is no longer simply a collection of modules. It has several layers of responsibility. At the bottom is the persistence and tenancy model. Above that are the operational domains. Around those domains is event-driven coordination. At the centre of the financial side is the accounting engine. Above the product itself sits the identity trust infrastructure. The Gateway provides a boundary between Core and independently deployed products, and those products can now develop independently while still participating in a common ecosystem.
That is a very different system from where the project started.
What I Learned
The biggest lesson from building Core is that architecture is mostly about boundaries. Not diagrams. Not frameworks. Not whether a system uses microservices. Boundaries.
Where does responsibility belong? What should one module know about another? What should happen synchronously? What should happen asynchronously? Where should data be authoritative? How should an event survive a failure? Who is allowed to access a tenant? How does another product establish trust? What belongs inside the platform, and what should become an independent product instead?
These questions shaped Core far more than the choice of Node.js, PostgreSQL, or any individual framework. The technologies are important. The decisions behind them are more important.
The Architecture Is Still Evolving
Monesize Core is not a finished artifact, and that's one of the reasons I find the project interesting. The architecture has already moved through several distinct stages. It started as a smaller product. It became a modular financial and operational platform. The accounting engine became a central component. Event-driven coordination became necessary as domains expanded. Compliance became part of the platform. Identity evolved from application authentication into an ecosystem trust problem. The Gateway emerged. Independent products became possible. Desk became one of the first concrete examples of that model.
Future products can now be introduced without requiring every product to become another component of Core itself. That is probably the most important evolution of all. Core is becoming a foundation rather than simply an application.
Looking Forward
The next stage is not necessarily about making Core bigger. It is about making the boundaries better. As more products are introduced into the ecosystem, the identity layer will become more important. As event volume increases, event processing and observability will become more important. As workloads grow, performance characteristics will become more important. As products become more independent, the interfaces between them will become more important. And as the platform handles more financial and operational responsibility, correctness will become increasingly important.
That means the architecture will continue to evolve. Some parts will eventually deserve stronger isolation. Some components may become independent infrastructure. Some workloads may justify different processing models. But those decisions should emerge from actual system requirements rather than architectural fashion.
Closing
Monesize Core is the largest software system I have designed and built, and its most interesting parts are not individual features. They are the decisions connecting those features together.
The accounting engine exists because operational activity needs financial structure. The event-driven architecture exists because business consequences need to be processed reliably. The Transactional Outbox exists because important events cannot simply disappear when a downstream operation fails. Tenant isolation exists because a multi-tenant system has to make data boundaries explicit. The identity trust layer exists because independent products should not need access to Core's authentication secrets. The Gateway exists because an ecosystem needs a controlled trust boundary. Desk exists independently because not every product needs to live inside Core. And the modular monolith exists because independent domains do not automatically justify independent services.
The architecture has changed as the product has changed. That is the real story of Monesize Core. It is not a story about choosing the perfect architecture on day one. It is a story about building a system, discovering what the system actually needed, and letting the architecture evolve around those requirements.
Comments
Post a Comment