At a glance

The company
Radicant is a Swiss digital bank built around sustainable investing, offering everyday accounts in multiple currencies, savings products, discretionary investment portfolios, custody accounts, and Swiss retirement products: all through a mobile-first experience.
Like every licensed bank, Radicant's system of record is a traditional core banking platform: a relational schema with cryptic German abbreviations for table and column names, dozens of code-translation lookup tables, and decades of accumulated domain logic. Like every digital-first bank, Radicant's product expects that data to look nothing like that. The app shows a clean, categorized, human-readable transaction feed — merchant names, recipient IBANs, card details, exchange rates, fees, running balances — updated the moment something happens on the account.
Closing the gap between those two worlds, in under a second, is the pipeline this story is about.
A note on names: the pipeline described here was designed and built in Portugal by what was then Radicant Innovation Hub — the bank's dedicated development team — and today operates as Naura Innovation Lab. Throughout this story, Radicant refers to the bank and its core banking platform, and Naura Innovation Lab refers to the team that built the streaming pipeline.
The challenge: business logic trapped in the database
Radicant's original transformation layer lived inside SQL Server itself:

Functionally, it worked. Operationally, it created a set of problems that compound as a bank grows.
The transformation competed with the transactional workload. Every enrichment ran as triggers and stored procedures on the same SQL Server instance handling live banking operations. Read amplification from the transformation logic and the write path for core banking shared the same resources — never a comfortable place to be when the database is the system of record for customer money.
The logic was effectively unmaintainable. The transformation that produces Radicant's enriched transaction feed touches 30 source tables through more than 70 join clauses, with 16 nested CASE expressions driving transaction categorization alone. Expressed as procedural T-SQL spread across triggers and stored procedures, that logic had no version-controlled lineage, no unit of deployment smaller than "the whole procedure," and no way to test a change without touching the production database.
Two transaction lifecycles, two code paths. Retail banking transactions exist in two states, and customers care about both. A card authorization or an in-flight payment lands in the pending ledger and needs to appear in the app immediately. Later, the same economic event settles into the booked ledger. Radicant's feed unions both — which meant maintaining two near-identical branches of very complex logic and keeping them in sync forever.
Extra hops, extra latency, extra state. Writing processed results back into SQL Server tables just so Debezium could read them again meant another set of physical tables to size, index, back up, and reason about, plus the latency of the round trip.
The goal Radicant set was direct: replace the triggers, stored procedures, and internal streams with RisingWave, leaving SQL Server to do only what it should — be the transactional system of record.
Why RisingWave
The Naura Innovation Lab team's evaluation came down to four things.
PostgreSQL-compatible SQL, not a new framework. The logic being migrated is thousands of lines of hairy analytical SQL, and rewriting it in a JVM streaming API was never on the table. RisingWave let the team port the existing queries as SQL — correlated subqueries against free-text tables, STRING_AGG, substring-based code parsing, deeply nested CASE logic and all — rather than reimplementing them from scratch.
Materialized views as the unit of deployment. Each transformation becomes a named, incrementally maintained materialized view. That maps cleanly onto how analytics engineers already think, and it decomposes the monolith: staging views per source table, then intermediate views per business concept.
Native SQL Server CDC. RisingWave connects directly to SQL Server's change data capture, removing Debezium and the intermediate processed tables from the critical path.
dbt as the control plane. dbt-risingwave let Radicant manage streaming materialized views with the same project structure, tags, lineage graph, and CI they already use for batch analytics — including zero-downtime deployment of model changes, which turned out to matter as much as the engine itself.
The new architecture
As the above figure shows, the staging → intermediate → serving flow and its three downstream sinks are presented in the context of the full before-and-after architecture.
Two things disappeared from the diagram: the stored procedures and the processed SQL Server tables that existed only to be re-read by Debezium.
From the unified serving layer, Radicant writes to three downstream systems: Kafka for event delivery, PostgreSQL as the transactional database, and BigQuery as the data warehouse for analytics.
Inside the pipeline
The enriched transaction feed is not a simple projection. For every booking, the pipeline resolves:
Account and customer context: account type, IBAN, portfolio, customer key, and current balance, where balance itself is the sum of the settled balance and the pending balance, so that in-flight activity is reflected correctly.
Human-readable text: booking text and payment purpose pulled from the core system's free-text table, filtered by language and text type, with fallbacks through
COALESCEto external text and finally to the translated booking-type description.Counterparty details: recipient name, recipient IBAN, and correspondent bank address, resolved through the payment address tables with version-matched joins, plus fallback logic for reference-number-only payments.
Card and merchant data: masked card number, card type, card currency, and merchant category code from the card authorization and ATM message logs.
Money mechanics: amount, debit/credit direction, value date, currency, exchange rate (a three-level
COALESCEacross transaction, payment, and account currency conversions), and transaction costs aggregated from the fee memo table.Categorization: a priority-ordered
CASEthat maps origin codes, booking types, card flags, and account types onto the categories the app displays: ATM withdrawals, card payments, instant transfers, portfolio funding and withdrawals, interest, fees, and retirement account transfers.
All of it maintained incrementally. All of it expressed in SQL.
Decomposing the monolith
The original migration target was a single 700-line model that unioned the booked and pending branches and did every transformation in one query. It worked, but it was a single unit of failure and a single unit of recompute.
The refactor split it along the natural seams:
core_bookings: settled bookings, joined and enrichedcore_pending: pending bookings, joined and enrichedtransaction_cost: fee aggregation, referenced by both
Each is a materialized view in the intermediate schema, tagged ['ods', 'bookings'], with dbt ref() wiring the lineage. Shared logic is computed once and reused instead of being duplicated across two branches.
Performance patterns that mattered
Getting a 30-table streaming join to behave took deliberate query engineering. The patterns that made the difference:
Aggregate before joining to prevent fan-out. The payment tables, including payment orders, payment items, card authorizations, and order master, have a one-to-many relationship with a booking. Joining them naively multiplies rows, and in a streaming system, row multiplication also multiplies state. The team collapsed them into a single pre-aggregated view keyed on the payment order, folding the detail into one row per booking before it ever reaches the main join:
-- Illustrative;
payment_details AS (
SELECT
po.order_id,
po.bank_id,
MAX(pi.payment_ref_id) AS payment_ref_id,
SUM(pi.amount_payment_ccy) AS total_amount,
COUNT(DISTINCT pi.payment_ref_id) AS payment_count,
STRING_AGG(DISTINCT pi.reference_no, ',') AS reference_numbers
-- card, order, and FX fields folded in the same pass
FROM filtered_payment_orders po
LEFT JOIN filtered_payment_items pi USING (order_id, bank_id)
-- card authorization and order master joins
GROUP BY po.order_id, po.bank_id -- plus currency and direction keys
)
Filter early, and push the filter upstream. A time-bounded predicate on the driving table shrinks the working set at the source rather than at the end, and the resulting booking keys are then used to prune the payment tables before they are joined at all.
Use EXISTS, not IN, for pruning. Semi-joins against the extracted booking-key set keep the pruning step from materializing intermediate results it doesn't need.
Cast join keys once, upstream. A numeric code joined against a text column forces a per-row cast inside the join. Pre-casting it in a small dedicated view moves that work out of the hot path.
Scan shared source tables once. The free-text table is needed twice: for booking text and for payment purpose. Rather than using two filtered scans, one view applies the shared predicates, and two thin views split the data by text type.
Unaligned joins where barrier alignment isn't worth it. For this model, SET streaming_enable_unaligned_join = true as a pre-hook reduced the latency cost of the wide join graph.
Deploying changes without stopping the feed

A streaming pipeline raises a question that batch pipelines don't have to answer: what happens to the feed while you deploy a change to it? A materialized view that has to be dropped and rebuilt stops producing, and for a bank, a gap in the transaction feed means a gap in what customers can see in the app.
Radicant deploys through dbt's zero-downtime mode, which turns a model change into a swap rather than a rebuild:
dbt runpicks up the new model version from version control.RisingWave builds a temporary materialized view alongside the live one. The live view keeps serving Kafka, PostgreSQL, and BigQuery the whole time.
The temporary view backfills from the CDC source until it has caught up.
An atomic swap exchanges the two names.
The old view is dropped.
Downstream consumers never observe a gap: they see the old definition, then the new one. Combined with dbt lineage, this makes changing transaction categorization logic an ordinary pull request rather than a maintenance window: the logic is reviewed like code and ships without the feed going quiet.
Results
Radicant's enriched transaction pipeline now runs on RisingWave in production:
Under 1 second end-to-end, from a committed change in the core banking database to an enriched message on Kafka — against a requirement of 5 seconds. In product terms: a customer sees a card authorization, a payment, or a balance change in the app effectively as it happens, rather than waiting on a batch of stored procedures to catch up.
Production volume is handled comfortably at roughly 150K change messages per day, 70% of which arrive inside a single 2-hour window. The bursty shape of banking traffic is the hard part, and it is absorbed without latency degradation.
Two systems removed from the critical path: the trigger and stored-procedure layer inside SQL Server, and the processed tables plus Debezium hop feeding Kafka.
The transactional database does one job again. Enrichment no longer competes for resources with live banking operations.
Business logic moved into version control and was centralized on RisingWave. Logic that had been spread across database procedures and other tools was reorganized into dbt models with lineage, tags, and reviewable diffs. This made the pipeline easier to maintain and organize: a change to transaction categorization is now a pull request, not an
ALTER PROCEDUREagainst the core banking database.

