Engineering Trust in Financial Systems · Paper 5
Lessons from Badgerlane, an independently built fintech application.
Riley Branch · First edition · published
Published 2026-08-05
An evidence-based case for a modular monolith with explicit internal contracts and concrete criteria for earning a remote service boundary.

The Safe Monolith
Architectural restraint, internal boundaries, and knowing when to extract a service
Abstract
Architecture discussions often treat a monolith as a temporary condition and microservices as the natural destination. That framing skips the decision that matters: which boundaries reduce risk for this system, team, and operating model?
This paper describes the evolution of a fintech application that kept its React frontend, Express API, session handling, product workflows, and Postgres data in one deployable application while extracting only a deterministic Python rule engine behind an HTTP contract.
The monolith reduced operational and transactional complexity for a lean team. It did not eliminate the need for modularity. In fact, the case study also shows the cost of allowing central route and storage files to grow faster than internal boundaries.
A safe monolith is not one large module. It is one deployment unit with deliberate internal contracts, constrained dependencies, visible failure modes, and explicit criteria for extraction.
1. Deployment topology is not architecture quality
Two ideas are often collapsed:
- deployment topology: how many independently deployed processes exist;
- modularity: how clearly responsibilities and dependencies are separated.
A system can be:
- a modular monolith;
- a tangled monolith;
- a well-bounded service architecture;
- a distributed tangle.
Splitting a large file into a network service does not create a good boundary. It adds latency, partial failure, version compatibility, authentication, telemetry, deployment, and data-consistency concerns to whatever coupling already existed.
The question I use is not:
When do we graduate from the monolith?
It is:
Which boundary has earned the operational cost of becoming remote?
That keeps architecture tied to evidence rather than fashion.
2. The case-study shape
The application started and largely remained one deployable web application:
System shape: The React browser client connects to the Express application. The application owns local Postgres state and integrates with Stripe, Quiltt, Amazon SES/SNS, and a separately deployed Python rule engine.
The application process owns:
- server-authoritative sessions;
- authentication and step-up verification;
- consumer and advisor workflows;
- organization and assignment authorization;
- billing and entitlement projections;
- financial-data ingestion and local read models;
- monthly plan lifecycle;
- notifications, support, and audit surfaces;
- static delivery of the built React client.
Postgres is the system of record. Stripe and Quiltt remain upstream authorities for their respective provider domains. The Python rule engine calculates routing recommendations over HTTP.
This is not a purely local system. Provider APIs and webhooks already make it distributed. That was an argument for caution: every new internal service would add another remote boundary to a system that already had several.
3. Why one deployable application was the safer default
3.1 One identity and session model
The browser authenticates through a server-side session stored in Postgres. The same application evaluates consumer access, advisor assignment, support scope, organization administration, entitlement, and step-up proof.
Splitting those workflows early would require either:
- a shared session dependency across services;
- token exchange and service authentication;
- duplicated authorization logic;
- a centralized authorization service;
- or inconsistent decisions.
None of those options was impossible. None solved a demonstrated scaling or ownership problem at the time.
3.2 One transaction boundary
Many workflows update closely related state:
- accept consent and advance onboarding;
- create a plan version and adjustment history;
- apply a webhook projection and update entitlement;
- create an organization assignment and audit event;
- persist a support request and outbound-email record.
A single Postgres transaction cannot make an external provider call atomic, but it can keep local invariants together. Splitting local state ownership across services would introduce sagas, outboxes, compensation, and reconciliation where ordinary transactions were currently sufficient.
3.3 One release artifact
The production image contains the built client and API. The same artifact runs database migrations and then serves application traffic.
For a lean team, that yields:
- one version to identify;
- one main rollback target;
- one local startup path;
- one release gate;
- one application log stream;
- one place to enforce runtime configuration.
This is not merely convenience. Every additional deployable increases the number of version combinations that can exist during rollout and recovery.
3.4 Lower coordination cost
The same team owned most of the product. Independent service deployment would not have created independent organizational ownership. It would have made one team coordinate with itself over network contracts.
Service boundaries are most valuable when they align with meaningful differences in scale, reliability, security, data ownership, runtime, or team responsibility.
4. The boundary that did earn extraction
The routing rule engine became a small Python service behind RULE_ENGINE_URL.
That boundary had concrete reasons:
- a distinct deterministic calculation domain;
- a language and library choice different from the Node application;
- an input/output contract that could be tested independently;
- no need for direct access to the application database;
- a fail-closed application response when the engine is unavailable;
- a plausible future need to version or scale calculation separately.
The application prepares the input, calls the engine, validates the output, and stores the proposal and later workflow state. The engine does not own sessions, tenancy, approval, or execution authority.
That narrow responsibility is what makes the remote boundary defensible.
The extraction did add cost:
- another image;
- another ECS service;
- health and network configuration;
- runtime URL management;
- deployment and smoke checks;
- new failure behavior.
The calculation boundary was valuable enough to pay that cost. Most application modules were not.
5. Use an extraction test, not instinct
I evaluate a candidate service against several questions:
| Question | Evidence that supports extraction |
|---|---|
| Does it need independent scale? | Sustained load differs materially from the main app |
| Does it need failure isolation? | A failure should not consume app resources or block unrelated traffic |
| Does it have distinct data ownership? | It can own its data without distributed transactions across common workflows |
| Does it need a different runtime? | Language, libraries, hardware, or process model create real value |
| Does it need independent release cadence? | Changes can safely deploy without coordinated app changes |
| Does it have a stable contract? | Inputs, outputs, errors, and compatibility rules are understood |
| Does a different team own it? | Team boundaries are durable enough to justify service autonomy |
| Does the security boundary improve? | Isolation meaningfully reduces privilege or exposure |
One “yes” is rarely enough. A service should have a coherent reason to exist remotely.
I also ask the inverse:
- Which transactions become distributed?
- Which failures become partial?
- Which credentials are duplicated?
- Which development workflows become harder?
- Which on-call signals and runbooks are added?
- What happens when the service is one version ahead or behind?
The extraction decision includes both columns.
6. Keep external providers behind local contracts
Even inside one application, provider SDKs should not define the product’s domain model.
The case study uses local boundaries for:
- Stripe subscription and entitlement policy;
- Quiltt connection, account, transaction, and financial-detail ingestion;
- a neutral aggregation read model;
- email delivery state;
- rule-engine inputs and outputs.
This makes the monolith less coupled to its providers. The browser does not receive provider secrets. Product routes consume local application state rather than making every request depend on live Stripe or Quiltt calls.
The important modularity is:
Provider boundary: provider payload → adapter → local domain projection → product workflow
Moving the adapter into another container would not improve that boundary by itself.
7. A monolith still needs internal architecture
The safest version of this system would organize code around domain modules:
auth/tenancy/billing/aggregation/planning/notifications/support/audit/
Each module would expose:
- route registration;
- application services;
- storage interfaces;
- domain types and state transitions;
- integration adapters;
- focused tests.
Cross-module calls would flow through those interfaces rather than importing arbitrary storage functions or provider clients.
Shared infrastructure—database connection, request IDs, session middleware, logging, error envelopes—would remain platform code rather than a grab bag of domain utilities.
This is where the case study is instructive in an uncomfortable way.
8. The large-file warning
In the implementation snapshot used for this paper:
server/routes.tsexceeded 17,000 lines;server/storage.tsexceeded 5,000 lines;shared/schema.tsapproached 2,700 lines.
These figures describe the point-in-time implementation snapshot reviewed for this paper; they are not presented as current repository metrics.
The application had begun extracting route modules for organization administration, tenant resolution, and site administration, and it had separate services for cash flow, routing execution, billing policy, webhooks, and provider normalization. But the central files still carried too much change traffic.
This is not proof that the monolith should become microservices.
It is proof that deployment restraint does not excuse code-structure debt.
Large central files create:
- merge contention;
- broad review surfaces;
- unclear ownership;
- accidental dependency reach;
- difficult focused testing;
- pressure to copy patterns instead of understanding them.
The next architectural move should be internal extraction:
- move one route family at a time behind a registration module;
- define the application service it needs;
- narrow its storage interface;
- keep the same database and process;
- preserve behavior with focused tests;
- measure whether the boundary reduces change coupling.
If that module later earns remote deployment, the contract will already exist.
9. The database is shared; ownership should not be vague
A modular monolith can use one database without treating every table as globally mutable.
Logical ownership can still be explicit:
- billing code owns subscription and entitlement projections;
- aggregation code owns provider connection and transaction caches;
- planning code owns proposals, plan versions, approvals, and execution records;
- tenancy code owns organizations, memberships, and assignments;
- audit code defines event envelopes and append behavior.
Other modules should read through a service or stable query contract where practical. Direct table access may remain for performance or reporting, but it should be visible as a dependency.
This discipline makes migrations easier to assess. A schema change has an owner, known consumers, and a compatibility story even though one deployment applies it.
10. Design failures as part of the contract
Local function calls fail too, but remote calls add latency, timeout, and network ambiguity.
For the rule-engine boundary, the application needs explicit behavior for:
- engine unavailable;
- timeout;
- invalid response;
- partial or low-confidence inputs;
- unsupported rule version;
- retry;
- health degradation.
The chosen posture is fail closed: no routing recommendation is safer than a fabricated fallback when the calculation service cannot be trusted.
That same discipline applies to provider integrations. A module contract should define bounded errors and recovery states even when it runs in process. Doing so makes the application safer now and makes later extraction less surprising.
11. Testing a modular monolith
One deployment unit supports several useful test layers:
- unit tests for calculation and policy;
- module-level tests against narrow dependencies;
- endpoint integration tests with a real Postgres schema;
- browser tests for cross-module user journeys;
- release smoke against the built application;
- contract tests for the remote rule engine and external-provider adapters.
The test portfolio should follow risk, not architecture vocabulary.
For example:
- cross-tenant denials prove authorization boundaries;
- webhook duplicate and error cases prove provider projection behavior;
- plan versioning tests prove workflow state;
- migration-first release tests prove deploy compatibility;
- app-map drift detects routes that changed without product evidence.
A microservice is not automatically more testable. A boundary is testable when its inputs, outputs, state, and failures are explicit.
12. Operating the safe monolith
The operational model remains intentionally small:
- one main application dashboard and log stream;
- one application health contract;
- one primary release workflow;
- one schema migration sequence;
- one set of runtime secrets;
- a small companion-service health signal;
- provider-specific webhook and reconciliation views.
This lowers cognitive load during incidents. It also concentrates blast radius: an application-process failure can affect several product areas.
That tradeoff should be managed through:
- process health and restart behavior;
- health-aware rolling deploys;
- bounded provider timeouts;
- database connection limits;
- graceful shutdown;
- targeted rate limits;
- error isolation in background work;
- clear support and audit surfaces.
If one workload begins threatening the rest—large sync jobs, document generation, or high-volume event processing—that is concrete evidence for a worker or service boundary.
13. Signs that the monolith is no longer the safest choice
Extraction becomes more compelling when several of these are true:
- one workload saturates CPU, memory, or database connections independently;
- failures in one domain repeatedly degrade unrelated workflows;
- a queue-backed asynchronous workload needs separate scaling;
- teams cannot deploy independently without coordination;
- the shared database prevents a necessary reliability boundary;
- security requires materially different network or data privileges;
- release cadence differs enough to create persistent coupling;
- local modularization has produced a stable, narrow contract;
- operational maturity can support another deployable responsibly.
Line count alone is not on the list.
Large files demand modularization. Independent operational characteristics demand services.
14. Practical design rules
- Treat deployment topology and modularity as separate decisions.
- Keep one deployable while it reduces more risk than it creates.
- Build internal domain contracts before remote service contracts.
- Extract only when scale, failure, runtime, security, data, cadence, or team ownership provides concrete evidence.
- Count the new partial failures and operational artifacts before extraction.
- Keep provider schemas behind local adapters and domain projections.
- Give shared-database tables logical owners.
- Fail closed at consequential remote boundaries.
- Use large central files as a signal for internal refactoring, not automatic distribution.
- Let observed operating pressure—not architectural status—drive the next deployable.
Conclusion
The monolith was not the absence of an architecture decision. It was a decision to preserve one identity model, one local transaction boundary, one main release artifact, and one operating surface while the team and product were still tightly coupled.
The separate rule engine shows the other side of the judgment. A distinct runtime, stable calculation contract, and meaningful failure boundary justified another service.
The central code files show the cost of restraint without enough internal modularity. That debt should be addressed directly, inside the process, before a network is used to disguise it.
A safe monolith is neither a starter architecture nor an ideological destination. It is a system whose boundaries are deliberate, whose failures are visible, and whose next extraction must earn its complexity.