Fresha

How Fresha moved customer-facing analytics off Postgres with StarRocks

Fresha moved customer-facing home-page analytics from its Postgres OLTP database to StarRocks internal tables fed by Debezium, Kafka and Flink. Queries that took 15–20 seconds or timed out for the largest partners now return in about 200 ms with minute-level freshness.

200ms

home-page analytics queries

About 200 ms, even with the heaviest filters and aggregations, at minute-level freshness

15–20s

previous load time for the largest partners

Home-page analytics served from Postgres, often ending in timeouts

<1s

p95 latency after the switch

Feature-flag comparison with the Postgres-backed path; p95 dropped to about sub-second and the p99/p99.9 spikes disappeared

CompanyFresha
Use caseCustomer-facing analytics
Previous solutionPostgreSQL (OLTP) and Snowflake
ReplacedPostgreSQL for home-page analytics

Originally published at: Fresha Data Engineering on Medium

About Fresha

Fresha, headquartered in London, runs a booking and business software platform for the beauty and wellness industry. Salons, barbers, spas and other businesses use it for scheduling, payments and client management, and consumers use its marketplace to find and book appointments.

Postgres was serving both transactions and analytics. Moving the hot analytics path to StarRocks internal tables fixed home-page latency for the largest partners and took that load off the OLTP database.

Key takeaway · Keep analytics off the transaction path

The original post was written by Anton Borisov, with credit to Fresha's data engineering team: Emiliano Mancuso, Nicoleta Lazar, Samuel Valente, Paritosh Anand, Piotr Królikowski and Emil Smoleński.

Fresha's data engineering team has run StarRocks in production since spring 2025. As far as the team knows, that made Fresha one of the first companies in the UK to do so.

The challenge

By the middle of 2024, analytics at Fresha ran on two systems, and each covered only part of the need.

Snowflake handled classic BI and large exports. For product and operational analytics that issue many small queries and need near-real-time data, it was neither fast enough nor cost-effective.

The customer-facing PostgreSQL database took on the rest. Ad-hoc analysis and product dashboards sent wide joins and heavy aggregations to the same database that processed transactions. During traffic peaks this caused head-of-line blocking and noisy-neighbor effects: dashboards slowed down unpredictably, and transaction paths, checkout included, were sometimes slower too.

The team wrote down what a new analytics layer had to do:

  • Take historical analytics off the OLTP path so Postgres is protected.
  • Treat open table formats on object storage (Iceberg and Paimon) as the source of truth.
  • Be easy for engineers to adopt: MySQL protocol, standard drivers, little new tooling.
  • Absorb traffic spikes without long capacity planning.
  • Support tiered freshness: the hottest paths within seconds to minutes, everything else within minutes.
  • Keep operations light, with fewer custom pipelines and special-purpose systems.

The solution

After a short evaluation, the team chose StarRocks for a few reasons. It speaks the MySQL wire protocol, so engineers and most BI tools connect to it directly. It can query open table formats in place for history and keep internal columnar tables for the lowest-latency paths. Its materialized views support transparent query rewrite, and in shared-data mode compute can scale without moving data around.

Architecture

Debezium captures changes from Postgres and publishes them to Kafka as Avro, with a Schema Registry. Flink and Spark consume from Kafka and write to the lakehouse, and StarRocks reads the lake through external catalogs.

Fresha's data flow in four planes: a Sources Plane with PG and a Kafka Connect Plane (Kafka, Schema Registry, Debezium); a Compute Plane with Flink and Spark; a Lake Plane with Iceberg and Paimon; and a Sinks Plane with PG, Kafka, Iceberg and StarRocks, which reaches the Lake Plane through an external catalog
High-level data flow at Fresha

From the Kafka spine, data moves along three lanes:

  • Real-time: Flink writes into StarRocks internal tables. Primary-key tables hold current rows, aggregate-key tables hold pre-rolled metrics, and duplicate-key tables take append-heavy streams. Dashboards and APIs read from them, with materialized views for rollups and pre-joins. Object storage is not on this path.
  • Historical: Flink lands operational CDC in Paimon. Iceberg receives long-term facts and slowly changing dimensions from both Flink and Spark, and Spark runs backfills, repairs and compaction. StarRocks queries both formats through external catalogs, and materialized views are refreshed after backfills.
  • Text search: Flink and Spark index data into Elasticsearch. StarRocks exposes it through its experimental Elasticsearch catalog, so search results can be joined with other data in SQL.
Four stacked lanes: an ingestion spine from PG through Schema Registry and Debezium to Kafka; a real-time lane where Flink writes to StarRocks internal tables read by dashboards; a historical lane where Spark and Flink write to Iceberg and Paimon, queried through StarRocks external catalogs; and a text search lane where Spark and Flink write to Elasticsearch, queried through the StarRocks ES catalog by federated queries
Ingestion spine and the three lanes

StarRocks is the single MySQL endpoint in front of all three. That gives Fresha three freshness tiers: hot data in seconds through Kafka, Flink and internal tables; warm data in minutes from StarRocks over Iceberg and Paimon, by federation or optional materialized views; and deep history in Iceberg and Paimon, with Spark backfills.

Home-page analytics

The first workload was the home page that Fresha partners see. It shows top employees compared over two months, top services and a feed of recent sales. On Postgres these widgets were fine for small accounts. For the largest partners, pages took 15 to 20 seconds to load or timed out, and the cold queries pushed useful pages out of the database's buffer cache, which hurt transactional work.

The requirement for the move was minute-level staleness. The team first tried serving the widgets from Iceberg. It worked, but frequent small files and the resulting compaction load made minute-level freshness unreliable. The hot path moved to StarRocks internal tables, and Iceberg and Paimon stayed as the historical record.

The team did not use materialized views here. Instead, a stack of SQL views sits on the internal rt_sales table, which Flink keeps current from the Debezium and Kafka stream. vw_sales_enriched applies business joins and status rules, vw_recent_sales defines what counts as recent, and vw_top_employees_2m and vw_top_services build the widgets. Developers reuse the same definitions, and the optimizer pushes filters and projections down through the views.

Layered views for home-page analytics: rt_sales (StarRocks internal, CDC upserts via Debezium, Kafka and Flink) feeds vw_sales_enriched (business joins and status rules), which feeds vw_recent_sales (recent window and eligible statuses), which feeds vw_top_employees_2m (2-month comparison) and vw_top_services (service rankings)
Layered views for the home page

Operating practices

The team settled on a few habits. Schema changes go through a migration tool with up and down SQL scripts; it records the schema version in StarRocks, waits for asynchronous ALTER jobs to finish and rolls back on failure. Query tuning starts from EXPLAIN ANALYZE profiles and metrics such as bytes scanned, partitions touched, join type and P50/P95 latency. Tables are partitioned by time and distributed by business keys such as provider_id. Large-to-small joins use broadcast, and large-to-large joins use colocation or bucket shuffle. Bloom filter and bitmap indexes are added only when a profile shows they help. Schema evolution relies on Schema Registry compatibility checks, versioned views switched by pointer, idempotent Flink sinks and CI gates.

StarRocksPostgreSQLDebeziumApache KafkaSchema RegistryApache FlinkApache SparkApache IcebergApache PaimonElasticsearch

The results

Home-page analytics queries now return in about 200 ms, even when users apply the heaviest filters and aggregations, and the data is at most minutes old. Postgres no longer acts as a cache for analytics.

Fresha compared the two paths behind a feature flag. Postgres-backed queries showed frequent spikes of several seconds. With StarRocks enabled, p95 latency dropped to roughly sub-second and the p99 and p99.9 spikes went away.

Latency percentiles (p50, p75, p90, p95, p99, p99.9, max) in seconds from late May to mid-July: frequent spikes up to about 15 seconds on the Postgres path, then flat low latency after the StarRocks path is switched on in July
Latency before and after enabling the StarRocks path

StarRocks is now part of the team's everyday analytics work, serving real-time tables, lakehouse history and search from one SQL endpoint.