The legacy reality in insurance
In an insurance core, the product is largely a rules engine: rating tables, underwriting criteria, rounding conventions, jurisdictional variations, commission schedules, and endorsement calculations.
Over many years, those rules accumulate across procedures, triggers, views, and batch chains. The precise scale varies by organization, but the operating pattern is familiar: business logic is distributed and poorly documented, batch sequencing is tightly coupled, integration occurs through shared tables, and regression confidence depends on people rather than repeatable tests.
| Characteristic | Common legacy condition |
|---|---|
| Business logic | Distributed across procedures, triggers, and views with incomplete documentation |
| Rating and underwriting | Conditional logic duplicated by jurisdiction or product, with gradual drift |
| Processing | Nightly policy, billing, and commission jobs with strict sequencing |
| Integration | Direct database links rather than stable service contracts |
| Change confidence | Slow regression cycles because automated coverage is limited |
| Knowledge | A small number of specialists understand critical procedures |
The work is not to migrate the database. It is to migrate the business decisions the database has been making on the insurer’s behalf.
Target-state architecture
The target state separates channels, orchestration, business services, rules, events, and data. Services own workflow and data access; a rules layer owns pricing, underwriting, and eligibility decisions that change frequently or require business approval. The legacy core remains available during transition, but its responsibility shrinks domain by domain.
| Legacy responsibility | Modern home | Reason for the move |
|---|---|---|
| Workflow and orchestration in stored procedures | Spring Boot or FastAPI business services | Versioned, testable logic with an explicit service contract |
| Rating, underwriting, and eligibility rules | GoRules JDM decision graphs, managed in BRMS and evaluated by Zen Engine | Governed rule changes without coupling every change to an application release |
| Nightly batch chains | Kafka or Event Hub with stream processing | Domain events become available without an overnight delay |
| Linked-server integration | REST or gRPC APIs behind a gateway | Consumers no longer depend on physical database schemas |
| Sybase ASE transactional storage | Managed PostgreSQL or an equivalent operational store | A supported platform with standard tooling and an open ecosystem |
| Reporting queries on the transactional core | A separate analytical store fed through CDC | Reporting load no longer competes with policy transactions |
| Manual regression comparison | Contract tests and golden-file rule tests | Changes can be assessed against known legacy outcomes |
From stored procedure to business service
The central modernization task is to separate workflow from decision logic. Services should retain orchestration and data access. Rules that change frequently or require actuarial or underwriting approval should become named, versioned decision graphs with explicit inputs, outputs, tests, and ownership.
Legacy · stored procedure
CREATE PROCEDURE sp_calc_premium
@policy_id INT
AS
BEGIN
SELECT @state = state_cd,
@driver_age = drv_age
FROM policy_master
WHERE policy_id = @policy_id
SELECT @base_rate = rate_amt
FROM rate_table
WHERE state_cd = @state
IF @driver_age < 25
SET @surcharge = @base_rate * 0.35
ELSE IF @driver_age > 70
SET @surcharge = @base_rate * 0.15
UPDATE policy_master
SET premium_amt = @base_rate + @surcharge
WHERE policy_id = @policy_id
END
Modern · service evaluates a published decision
@Service
class PremiumRatingService {
private final RateTableRepository rates;
private final ZenEngine rules;
PremiumQuote calculate(PolicyContext ctx) {
var base = rates.findLiabilityRate(ctx.state());
var input = Map.of(
"driverAge", ctx.driverAge(),
"baseRate", base,
"state", ctx.state());
var result = rules.evaluate(
surchargeDecisionId, input);
return PremiumQuote.from(
ctx.policyId(), base, result);
}
}
The Java example illustrates the boundary: a configuration-driven decision identifier, an engine call, and a typed result. Exact SDK signatures should be confirmed against the version selected for implementation.
Decision location and environment are configuration
rules:
engine:
mode: agent
agent-url: https://rules-agent.internal.example
project: auto-policy-rating
environment: ${DEPLOY_ENV:PROD}
premium-surcharge:
decision: auto/liability/age-surcharge
version: latest-published
The age brackets and surcharge percentages are authored and tested as a decision table. GoRules stores the published model as JDM JSON; business users work through the management interface rather than editing this representation directly.
Conceptual decision table output
{
"rules": [
{ "driverAge": "< 25", "surcharge": "baseRate * 0.35" },
{ "driverAge": "> 70", "surcharge": "baseRate * 0.15" },
{ "driverAge": "[25..70]", "surcharge": "0" }
]
}
What becomes explicit
- Every decision has a stable identifier, owner, version, and test suite
- The service contract defines the required context and returned result
- The rule version is retained with the quote for audit and diagnosis
- Product and jurisdiction variants are managed deliberately
What leaves application code
- Age bands, thresholds, and surcharge calculations that change often
- Copy-pasted rule branches across services or stored procedures
- Hidden database updates inside a calculation
- Release dependencies for routine approved rule changes
Migration strategy: strangler pattern and CDC
A big-bang rewrite concentrates technical, operational, and regulatory risk into one cutover. A safer approach moves one bounded capability at a time, compares its output against the legacy path, and retires the old responsibility only after agreed evidence is met.
The five phases
Inventory the stored-procedure estate
Build a dependency graph and classify procedures as data access, business-rule-bearing, or unused. Rank them by business criticality, change frequency, and blast radius.
Establish the event, synchronization, and rules platforms
Stand up the event backbone, historical backfill, CDC pipeline, observability, and reconciliation patterns. In parallel, establish the GoRules management workflow and connect the first service to Zen Engine.
Separate service workflow from decision rules
Start with a constrained slice such as rating for one product and jurisdiction. Move orchestration and data access into a tested service; move the actual rating decisions into governed decision tables.
Prove equivalence before cutover
Run legacy and modern paths against production inputs. Investigate differences and obtain actuarial, compliance, and operational sign-off against explicit acceptance criteria.
Retire responsibility from the legacy core
Disable and archive the superseded procedures, remove the CDC feed when it is no longer needed, and report the remaining legacy surface after every release.
Risks and controls
| Risk | Control |
|---|---|
| A rare rule is discovered late | Use shadow reconciliation before retirement and retain a controlled rollback path through an agreed audit cycle |
| Performance regresses on the new stack | Load-test every extracted service at production-equivalent volume before traffic moves |
| Data diverges during parallel operation | Monitor CDC lag, reconcile business outcomes, and alert on variance beyond the agreed threshold |
| Regulatory evidence is incomplete | Version every extracted rule and retain its calculation trace, approval, and effective date |
| Legacy intent is misunderstood | Pair domain experts and legacy specialists with the engineers performing extraction; record intent in decision records |
| Engineers become the gatekeeper for every rate or eligibility change | Externalize frequently changed decisions into governed rules with business ownership, review, and publication controls |
| The program loses momentum | Release domain by domain and report business outcomes rather than waiting for a full-platform launch |
Governance: preserve the reason
The migration is difficult because years of decisions were embedded in code without durable context. Rebuilding the rules in a modern language without recording their purpose only recreates the same risk. Each extracted rule should be paired with a concise decision record: what it does, why it exists, who owns it, the source of authority, and the condition under which it should be reviewed.
What good looks like
Where to start
- Inventory procedures and triggers; produce a dependency graph and business-criticality ranking.
- Select one high-value domain with a contained blast radius.
- Define the event, CDC, reconciliation, and rollback patterns before moving live traffic.
- Establish the GoRules management workflow and connect Zen Engine using configuration-driven decision identifiers and environments.
- Build tests from real historical inputs and outputs of the legacy procedure.
- Run in shadow mode and investigate every material variance.
- Pair legacy specialists with engineers and capture rule intent in decision records.
- Track the remaining legacy surface as a program-level measure after every release.