Building an Insurance Program Data Configurator: Notes on a Hard Insurtech Problem
Insurtech presents a class of engineering problems that are far from the typical CRUD application. They operate in a domain where correctness is a non-negotiable business requirement and complexity is the default state. One of the most interesting challenges is building tooling for Managing General Agents (MGAs). MGAs underwrite risk on behalf of insurance carriers, which means they must precisely define, configure, and manage "insurance programs"—the specific bundle of rules, coverages, limits, and pricing for a product like commercial property insurance in a given region.
The core task is to translate a carrier's dense, 200-page PDF underwriting guide into a dynamic, interactive, and foolproof digital system. This system must allow an MGA's internal experts to configure these programs without writing code, ensure every policy written against that program is valid, and adapt to constant changes in rates and regulations. This isn't just data entry; it's about encoding deep, conditional domain logic into a durable software artifact.
Architecting for Deep Configurability
Let's consider how to approach this with a modern, pragmatic stack: a Ruby on Rails backend API, an Angular/TypeScript frontend, and infrastructure managed with Terraform. The choice of stack is less important than the architectural principles, but these tools provide a solid foundation.
The Data Model: JSONB is Your Friend
The first temptation is to model insurance programs with a rigid SQL schema: tables for coverages, limits, deductibles, etc., with dozens of columns and foreign keys. This path leads to pain. Every new carrier or product variation requires a database migration. The logic becomes scattered across a sprawling object graph.
A more resilient approach is to embrace semi-structured data. PostgreSQL's JSONB type, managed through Rails/ActiveRecord, is ideal. We can define a core model, perhaps ProgramTemplate, that stores the *shape* of a program, and a corresponding ProgramConfiguration that stores the *values* for a specific MGA's instance of that program.
The ProgramTemplate could hold a structure based on JSON Schema to define the available fields, their types, and basic validation rules. It would also contain a uiSchema to give the frontend hints on how to render the form—grouping fields into sections, defining control types (slider, dropdown), and providing help text.
# A simplified ProgramTemplate's schema field (in JSONB)
{
"title": "Commercial Property Program",
"type": "object",
"properties": {
"location": {
"type": "object",
"properties": {
"state": { "type": "string", "enum": ["CA", "NY", "TX"] }
}
},
"building": {
"type": "object",
"properties": {
"construction_type": { "type": "string", "enum": ["Frame", "Masonry"] },
"coverage_limit": { "type": "integer", "minimum": 50000 }
}
},
"deductibles": {
"type": "object",
"properties": {
"standard": { "type": "integer", "default": 2500 }
}
}
},
"required": ["location", "building"]
}
A ProgramConfiguration would then store a JSONB document that conforms to this schema, representing a live, configured program. This approach gives us versioning, flexibility, and the ability to introduce new program types without touching the database schema.
The Frontend: A State Machine with a UI
The user experience for the configurator must be real-time. If an underwriter selects "CA" as the state, the available endorsements and deductible options must update instantly. This is where a robust frontend framework like Angular, combined with TypeScript's static typing, shines. TypeScript interfaces can be generated directly from the JSON Schemas, ensuring type safety between the Rails backend and the Angular frontend.
The configurator isn't a single form; it's a stateful application. The state of the entire configuration object is the central source of truth. As the user makes changes, the frontend can:
- Perform local validation: Use the JSON Schema to provide immediate feedback on simple constraints (e.g., "Coverage limit must be at least $50,000").
- Trigger API calls for dependent data: When the state changes, query a Rails endpoint like
/api/v1/templates/:id/derived_values?config=...to fetch updated dropdown options or conditional defaults. - Visualize complex rules: Clearly show which fields became available or invalid as a result of a change elsewhere.
While WebSockets or Turbo Streams could provide server-pushed updates, a well-designed async request/response model on field changes is often simpler and sufficient for this use case. The key is to keep the frontend responsive by offloading complex rule evaluation to the backend.
Where Things Break: Cross-Field Validation and Scale
JSON Schema handles basic validation well, but insurance logic is rarely basic. The real complexity lies in cross-field validation: "If construction_type is 'Frame' AND state is 'CA', then the minimum standard_deductible must be $5,000."
This is business logic, and it belongs on the server. In our Rails backend, we'd implement a set of validation service objects. These plain Ruby objects would take a configuration hash as input, traverse it, and apply a series of rules, returning a structured list of errors or warnings. This keeps the logic out of the models and makes it highly testable.
# app/services/program_validators/california_frame_deductible_validator.rb
class ProgramValidators::CaliforniaFrameDeductibleValidator
def self.validate(config)
errors = []
is_ca = config.dig('location', 'state') == 'CA'
is_frame = config.dig('building', 'construction_type') == 'Frame'
deductible = config.dig('deductibles', 'standard').to_i
if is_ca && is_frame && deductible < 5000
errors << { path: '#/deductibles/standard', message: 'Minimum deductible for Frame construction in CA is $5,000' }
end
errors
end
end
At scale, with thousands of potential fields and hundreds of cross-cutting rules, running these validations can become a performance bottleneck. The solution involves memoization, optimizing rule execution order, and potentially designing a more sophisticated rule engine if the complexity warrants it. However, starting with simple, explicit Ruby code is the most pragmatic path.
Pragmatism, Correctness, and the Human in the Loop
In a domain with such high stakes, engineering tradeoffs lean heavily towards correctness and auditability. An error in configuration could lead to writing millions in uninsurable risk.
This means:
- Immutability and Audit Trails: Every change to a
ProgramConfigurationshould be a new, versioned record, or at the very least, logged in a detailed audit table. Who changed the property coverage limit from $1M to $1.2M, and when? This is a core feature, not an afterthought. Libraries likepaper_trailfor Rails can be invaluable here. - Explicit Overrides: No system can capture 100% of the edge cases. An underwriter will inevitably need to handle a unique situation that the rules don't cover. The system must support this with an explicit "override" mechanism. An overridden value should be visually flagged in the UI and require a second approval from a manager. This "human-in-the-loop" workflow is critical for operational success. The data model needs a state machine (`draft`, `pending_review`, `approved`, `active`) to manage this lifecycle.
- Test Everything: The validation rules are the heart of the system. They need to be tested exhaustively with unit tests covering every logical branch. Integration tests should then verify the end-to-end flow, from an API request with a specific configuration to the expected validation error response.
A Reflection on the Problem
Building an insurance program configurator is a compelling technical problem because it forces a synthesis of data modeling, user experience, and complex business logic. It's not about replacing the expert underwriter. It's about building a tool that amplifies their expertise—a system that handles the tedious, error-prone task of checking hundreds of rules, freeing them to focus on the truly difficult part of risk assessment.
The solution isn't a magical AI but a well-architected system that makes complexity manageable. It encodes domain knowledge not as rigid, brittle code, but as flexible, versionable, and auditable data. That, to me, is the essence of good engineering in a complex domain.