In today’s fintech software development landscape, code robustness is not a luxury, but a compliance requirement. When designing a CRM intended for credit file management, the most common error is entrusting the file lifecycle to a messy series of boolean conditions. In this context, the Finite State Machine (FSM) emerges as the fundamental architectural entity to ensure that complex processes, such as mortgage disbursement, follow deterministic and secure paths. This article explores how to apply systems engineering principles to transform business logic into an unassailable workflow engine, reflecting experience gained in developing high-criticality platforms like the BOMA CRM.
The Problem of “Spaghetti Logic” in Financial CRMs
Imagine having to manage a mortgage file. In a naive approach, a developer might add columns to the database like is_approved, docs_uploaded, contract_signed. The resulting code to verify if a mortgage can be disbursed would look like this:
if (loan.is_approved && loan.docs_uploaded && !loan.is_rejected) {
// Disburse funds
}
This approach scales disastrously. What happens if the file is suspended? Do we add an is_suspended flag? And if it is reopened? The combination of N boolean flags creates 2^N possible states, most of which are inconsistent states (e.g., a file simultaneously “rejected” and “awaiting signature”). Finite state machines solve this problem by reducing the universe of possibilities to a directed graph of valid states and explicit transitions.
What is a Finite State Machine (FSM)?

An FSM is a mathematical model of computation. It is an abstract system that can be in exactly one of a finite number of states at any given time. The FSM changes state in response to external inputs; the change from one state to another is called a transition.
For a mortgage CRM, an FSM is defined by:
- States (S): {DRAFT, UNDERWRITING, APPROVAL, SIGNATURE, DISBURSED, REJECTED, CANCELLED}
- Events/Inputs (E): {SUBMIT_DOCS, APPROVE_CREDIT, CLIENT_SIGN, DISBURSE_WIRE}
- Transition Function (δ): A rule that defines: If I am in state X and event Y happens, I move to state Z.
Modeling the Mortgage Workflow


Instead of asking “which flags are active?”, we ask “what state is the file in?”. Here is how to model a standard mortgage flow:
- DRAFT: The broker is entering data. The only possible event is
SUBMIT_TO_UNDERWRITING. - UNDERWRITING: The bank analyzes documents. Possible events:
APPROVE(leads to APPROVAL) orREJECT(leads to REJECTED). It is not possible to go directly to DISBURSED. - APPROVAL: Credit is approved. Event:
ISSUE_OFFER. - SIGNATURE: The client must sign. Event:
REGISTER_SIGNATURE. - DISBURSED: Final positive state.
Transition Diagram (Logical Representation)
The power of finite state machines lies in implicit prohibition. If the system receives the event DISBURSE_WIRE while the file is in the UNDERWRITING state, the FSM must not merely fail silently: it must throw an Illegal Transition exception. This makes the system deterministic.
Technical Implementation: Patterns and Code
To implement an FSM in a modern backend stack (e.g., Node.js/TypeScript or Python), we advise against using giant switch/case statements. It is preferable to use the State Pattern or dedicated libraries like XState. Here is a conceptual implementation example in TypeScript:
type LoanState = 'DRAFT' | 'UNDERWRITING' | 'APPROVED' | 'DISBURSED' | 'REJECTED';
class MortgageFSM {
private state: LoanState;
private transitions = {
DRAFT: { SUBMIT: 'UNDERWRITING' },
UNDERWRITING: { APPROVE: 'APPROVED', REJECT: 'REJECTED' },
APPROVED: { DISBURSE: 'DISBURSED', CANCEL: 'REJECTED' },
DISBURSED: {}, // Terminal state
REJECTED: {} // Terminal state
};
constructor(initialState: LoanState) {
this.state = initialState;
}
public transition(event: string): void {
const nextState = this.transitions[this.state][event];
if (!nextState) {
throw new Error(`Invalid transition: Cannot execute ${event} from state ${this.state}`);
}
console.log(`Transition: ${this.state} -> ${nextState}`);
this.state = nextState;
this.onStateChange(nextState);
}
private onStateChange(newState: LoanState) {
// Hook for side effects (e.g., sending emails, webhooks)
}
}
Persistence and Database
At the database level (SQL), the most efficient representation is not a series of booleans, but a single indexed status column, often supported by an ENUM type to ensure referential integrity.
However, for complex audit trails (required by banking regulations), a separate loan_state_history table is advisable:
loan_id(FK)previous_statenew_statetrigger_eventtimestampuser_id(who triggered the transition)
Event-Driven Architecture and Side Effects
Integrating finite state machines with an event-driven architecture is where the CRM becomes proactive. Every valid state transition must emit a Domain Event.
The Reactive Flow
- User clicks “Approve File” in the dashboard.
- The API invokes the FSM:
fsm.transition('APPROVE'). - The FSM validates logic, updates DB, and if successful, emits the
LoanApprovedEventon a message broker (e.g., RabbitMQ or Kafka). - Decoupled consumers react:
- The Notification Service sends an email to the broker.
- The Document Service generates the approval PDF.
- The Audit Service logs the operation for compliance.
This decoupling prevents email sending logic from “polluting” the pure credit approval logic.
Prevention of Inconsistent States (Safety)
In the experience of developing systems like BOMA, we identified three golden rules for FSM safety:
- Atomicity: State transition and DB save must occur in the same database transaction. If the save fails, the in-memory state must be rolled back.
- Idempotency: If an external system sends the
APPROVEevent twice, the FSM must handle the second attempt gracefully (ignoring it or returning the current state), without creating duplicates or errors. - Guards: Beyond transition validity (A -> B), it is possible to implement “guards”. Example: “You can pass from UNDERWRITING to APPROVAL only if the sum of uploaded documents > 5”. Guards add a layer of controlled conditional logic within the rigid FSM structure.
In Brief (TL;DR)
Entrusting the mortgage lifecycle to boolean conditions generates critical errors and states that are impossible to manage effectively.
Finite State Machines transform complex processes into deterministic paths, ensuring secure transitions between different credit phases.
A software architecture based on defined states ensures data integrity and regulatory compliance in financial management platforms.
Conclusion

Adopting finite state machines in mortgage CRM development is not just a code stylistic choice, but a strategic architectural decision. It shifts complexity from error handling to the design phase, forcing engineers and product managers to define the business process with surgical precision before writing a single line of code. The result is a predictable, auditable, and, above all, secure system for managing financial assets.
Frequently Asked Questions

A Finite State Machine is a mathematical model that allows a system to be in a single defined state at a given moment, eliminating operational ambiguities. In the context of a mortgage CRM, it serves to manage complex flows by ensuring that files follow deterministic paths and preventing inconsistent states typical of management via simple boolean flags.
Using simple boolean flags creates what is defined as «spaghetti logic», generating an exponential number of state combinations that are often impossible to manage and validate correctly. An FSM reduces the universe of possibilities to a directed graph of valid states and explicit transitions, making the system more robust, secure, and easier to maintain compared to a messy series of conditional conditions.
To implement an FSM in modern stacks like Node.js or Python, using huge switch case structures is discouraged, preferring instead the State Pattern or dedicated libraries like XState. The best approach involves the rigid definition of states and transitions, ensuring that every state change is validated and can trigger domain events to integrate external services like notifications or document generation.
At the SQL database level, the most efficient practice consists of using a single indexed status column, often supported by an ENUM type to ensure data integrity, instead of multiple boolean columns. To meet banking compliance requirements, it is also fundamental to accompany this with a history table that records every transition, the triggering event, the timestamp, and the user responsible for the action.
To ensure data security, it is necessary to respect rules like atomicity, which ensures that transition and saving occur in the same database transaction, and idempotency, to manage duplicate attempts without errors. Furthermore, the use of logical guards allows adding specific conditions, such as the presence of minimum documents, before authorizing the passage from one state to another.




Did you find this article helpful? Is there another topic you'd like to see me cover?
Write it in the comments below! I take inspiration directly from your suggestions.