Design decisions
Architecture pages describe what a system is. This one says why, and what each choice was paid for with. A decision recorded without its cost is advocacy rather than documentation, so every entry below carries both.
Two deployment planes
Section titled “Two deployment planes”The decision. Reference data (channels, products, routing) is deployed once in Singapore. Transactional data (coverage, ledger) is deployed once per market.
Why. The two kinds of data have opposite requirements. A product definition should be identical in every market and is expensive to reconcile once copied. A policyholder record must not leave the country it was created in. One deployment cannot satisfy both, so the platform stops trying and separates them physically rather than by convention.
The cost. Every coverage issuance makes cross-plane network calls, and a control plane outage degrades every market at once. We accept a shared dependency on the reference layer in exchange for one definition of a product everywhere.
One database per service
Section titled “One database per service”The decision. Every service owns exactly one database. No service reads another’s tables. Cross-service data moves only through APIs.
Why. Adding a column to the product catalogue touches the product service and nothing else, because every other service only ever saw an API response. On a platform running the same catalogue across three countries with three regulators, that isolation is worth more than the round trips.
The cost. A single logical read becomes several network calls, and there are no joins across service boundaries. Anything that would have been one query is now composition in application code.
HTTP inside a request, events after it
Section titled “HTTP inside a request, events after it”The decision. Service-to-service calls that a caller is waiting on use HTTP. Everything downstream of a successful issuance goes onto Kafka.
Why. Accounting is not on the path between a customer tapping a button and a certificate coming back, so it is not on the request either. The ledger consumes events at its own pace and can replay the stream if it falls behind or needs reprocessing.
The cost. Anything reading from the event stream is eventually consistent, so a ledger figure can lag a coverage that exists. Reports built on that data must be read with that in mind.
HTTP rather than gRPC or a shared database
Section titled “HTTP rather than gRPC or a shared database”The decision. The boundary between two services is an HTTP contract.
Why. A shared database makes every schema change a cross-team negotiation, which is the exact failure the database-per-service rule exists to prevent. Against gRPC the argument is narrower: HTTP is what the insurers, channels and partners on the other side of our boundary already speak, and running one protocol at the edge and another inside adds a translation layer that buys little.
The cost. More verbose payloads, and no client generated from the transport itself the way a
gRPC stub falls out of a .proto. The second cost is what the OpenAPI document exists to answer,
and it is why the contract is a first-class artefact of the platform rather than documentation
written after the fact. See The integration surface.
An adapter per insurer
Section titled “An adapter per insurer”The decision. Every carrier is reached through its own adapter. No insurer’s format reaches the interior of the platform.
Why. Insurer systems are the least controllable dependency in this business and they differ from each other in ways that have nothing to do with insurance. Letting those differences leak inward would encode one carrier’s quirks into the model every other carrier is served by.
The cost. One adapter to write, test and maintain per carrier, and onboarding a new insurer is engineering work rather than configuration. This is the largest recurring cost on the list and we would make the same choice again.
The product lifecycle as data
Section titled “The product lifecycle as data”The decision. A product declares its own statuses and the transitions permitted between them. The coverage service contains no state machine; it validates a requested move against what the catalogue says.
Why. Adding a product, or a status to an existing one, does not require deploying the service that issues coverage. On a platform whose thesis is repeatable distribution across markets, that is the difference between adding a product in a day and adding one in a release.
The cost. The lifecycle is invisible in the code. A reader of the coverage service cannot tell you what states exist, and a misconfigured catalogue produces a runtime rejection where a hardcoded machine would have failed to compile.
No list endpoints on reference data
Section titled “No list endpoints on reference data”The decision. You read a channel or a product by identifier. Nothing enumerates the ones you were not given.
Why. The platform carries competing distributors and competing carriers at the same time. A list endpoint on shared reference data is a directory of who else is on it, and separation enforced by the absence of the endpoint is stronger than separation enforced by a filter someone has to remember to apply.
The cost. Identifiers are handed over during onboarding rather than discovered at runtime, and integrations cannot build a picker by asking the platform what exists.