Logistics & Supply Chain technology
Startups Logistics & Supply Chain July 15, 2026 • 12 min read

How Vahak Onboarded 800K Trucks Without Burning the Database

For: A Series A marketplace founder or CTO who has a working two-sided platform but is watching write throughput, onboarding latency, and ops escalations climb together as supply-side volume spikes — and cannot tell whether the bottleneck is the data model, the onboarding pipeline, or the matching layer

Vahak scaled truck onboarding to hundreds of thousands of supply-side records without degrading route-matching query latency by splitting the onboarding write path from the matching read path at the data model level — so ingesting a new truck never touched the indexes that live search depended on. The tempting fixes (bigger instances, read replicas, aggressive caching) were tried first. None of them held. What worked was a boring architectural call made early: treat onboarding and matching as two systems that happen to share a domain, not one system with two workloads.

This is a walkthrough of what broke, what we tried, what stuck, and what generalizes to any two-sided logistics marketplace watching write throughput and query latency climb in lockstep.

The setup: a truck marketplace that outgrew its own schema

Vahak is a logistics marketplace connecting truck owners and fleet operators with shippers who have loads to move across India. The product loop is deceptively simple. A truck owner posts availability — vehicle type, current location, destination preference, capacity. A shipper posts a load — origin, destination, weight, timing. The platform matches them. Money changes hands. Everyone moves on.

At hundreds of trucks, this works with any reasonable schema. A single Postgres instance, a couple of indexes on current_location and destination, and a matching query that runs in tens of milliseconds. Nobody thinks about it.

The problem starts when supply grows an order of magnitude faster than you planned. Truck onboarding is not a slow trickle. It comes in waves — driven by campaigns, referrals, regional expansion, and the network effect that kicks in once a critical mass of shippers is active. You go from onboarding a few hundred trucks a week to a few thousand a day. And the same tables that serve live matching queries are now getting hammered by writes.

The symptoms showed up in a predictable order:

Any one of those signals in isolation is a nuisance. Together, they're a message: the architecture wasn't built for the workload it now has.

What we tried first (and why it didn't stick)

The reflex when a database gets slow is to throw more database at it. We did that. Here's what happened.

1. Vertical scaling

We moved to a larger instance class. It helped for about three weeks. Then onboarding volume grew again and we were back where we started, just with a bigger bill. Vertical scaling buys you time, not a solution. If your workload is fundamentally contended, a bigger box just gets you to the wall faster.

2. Read replicas

The classic move: route reads to replicas, keep writes on the primary. This helped matching query throughput but introduced a new problem — replication lag. A truck owner would complete onboarding, get a confirmation, and then not appear in searches for anywhere from two to thirty seconds because the replica hadn't caught up. This is technically correct behavior. It's operationally unacceptable in a marketplace where the truck owner is watching their phone waiting to see themselves in results.

Worse, the matching query itself was expensive enough that even on a replica, it was slow when the working set didn't fit in memory. Read replicas solved a throughput problem we didn't quite have and made worse a freshness problem we did.

3. Aggressive caching

We put Redis in front of the hot matching queries. This worked well for popular routes — Mumbai to Delhi, Bangalore to Chennai — where the same query fired thousands of times a day. It did nothing for the long tail. And the long tail in Indian trucking is enormous. There are thousands of origin-destination pairs, and any given shipper's route has maybe a 40% chance of being one we'd seen that day. Cache hit rate plateaued around 55%. The other 45% still hit Postgres and still hurt.

Cache invalidation, as always, added its own bugs. A newly onboarded truck wouldn't show up until the relevant cache key expired. We were back to the freshness problem, just with a different mechanism.

4. Index tuning

We spent a week rebuilding indexes, adding partial indexes for active-only trucks, and using BRIN indexes on time-series columns. This bought real gains — probably 30% on the matching query. But every new index made writes slower. And the onboarding path was already the slower half of the equation. We were robbing Peter to pay Paul.

The call: separate the write path from the read path at the data model

The insight that changed things was uncomfortable because it required admitting the original data model was wrong — not buggy, not underoptimized, wrong for the workload it now had.

Onboarding and matching look like the same domain (trucks) but they are fundamentally different workloads with different access patterns and different consistency requirements:

We split the schema. Onboarding wrote to a normalized trucks_raw table optimized for inserts — minimal indexes, wide rows, no derived fields, no geospatial columns. A separate trucks_searchable table held the denormalized, index-heavy, query-optimized view that matching hit. An asynchronous pipeline moved records from raw to searchable, doing the enrichment (geocoding, route normalization, capacity bucketing) once at write time instead of on every query.

The critical property: inserting a new truck never touched an index that matching depended on. The two workloads stopped fighting each other for the same B-tree pages.

What the pipeline actually looks like

  1. Onboarding submission hits an API. RC book image goes to OCR. Basic validation runs synchronously. Record is inserted into trucks_raw. Response returns to the user in under two seconds. This table has one index — the primary key.
  2. An event is emitted to a queue (we used a hosted message broker; the specific choice mattered less than the pattern).
  3. A worker picks up the event, enriches the record — geocoding the location, normalizing the route into H3 cells, bucketing capacity, resolving vehicle type against a canonical list — and writes it to trucks_searchable. This is where the expensive indexes live: geospatial, composite indexes on (origin_cell, dest_cell, vehicle_type, active_status), and so on.
  4. Matching queries hit only trucks_searchable. They never see raw data. They never wait behind an onboarding write.

The typical lag from onboarding to searchable was under five seconds under normal load, and it degraded gracefully under bursts — the queue absorbed spikes that would have crushed a synchronous write path.

What this is bad at

Honest tradeoffs, because pretending there aren't any is how you get bitten:

What moved

Without quoting specific numbers I can't verify from memory, the qualitative shifts were:

Lessons that generalize to any logistics marketplace

1. Onboarding and matching are different products. Model them that way.

This is the point of the whole post. In any two-sided marketplace where supply-side ingestion and demand-side search share a database, you will eventually have to separate them. The only question is whether you do it before or after it hurts. Doing it early — while volumes are still moderate — is dramatically cheaper than doing it under fire.

2. Enrichment belongs at write time, not query time.

Every time you compute something during a matching query — geocoding a string, parsing a route, normalizing units — you're paying that cost multiplied by query volume. Move it to the write path, do it once, store the result. Query-time computation is a tax you pay forever.

3. Freshness requirements should be explicit, not assumed.

"Real-time" is not a requirement. It's a wish. The real question is: how stale can this data be before the user experience breaks? For truck search, five seconds is fine. For a payment confirmation, five seconds is not fine. Being honest about this unlocks entire categories of architectural choices.

4. The bottleneck is rarely where you first look.

We spent weeks tuning indexes, upgrading instances, and adding caches before admitting the data model itself was the problem. Every one of those interventions was defensible on its own. None of them addressed the root cause. If you're on the third round of optimization for the same problem, the problem is probably one level deeper than you've been looking.

5. Cold start matters more than steady state.

Marketplace cold start engineering is the discipline nobody writes about. Everyone benchmarks steady-state throughput. Almost nobody benchmarks what happens when you 10x supply in a quarter. If your architecture can't absorb an order-of-magnitude burst without operator intervention, it will hit a wall — the only variable is when.

How CodeNicely can help

If the situation described at the top of this post sounds familiar — a working marketplace, supply-side pressure climbing, and no clear signal on whether the fix is the data model, the pipeline, or the matching layer — the Vahak engagement is the closest analog in our portfolio. We worked on the platform through the phase where truck-side volume grew faster than any of the original assumptions accounted for. The write/read separation described here, the async enrichment pipeline, the H3-based route matching — those were choices made under real production pressure, not on a whiteboard.

What we bring to a marketplace at your stage is specifically the diagnostic work: figuring out whether the pain you're feeling is a data model problem, a pipeline problem, or a matching problem, because the fix for each is different and doing the wrong one wastes a quarter. If you're a logistics or marketplace startup watching the same symptoms, talk to us for a personalized assessment — no generic playbook, no assumption that what worked for Vahak works for you.

Frequently Asked Questions

When should a marketplace split its write path from its read path?

The signal is when write volume starts measurably degrading read latency, or when you find yourself adding indexes for reads and then removing them because writes got too slow. If you're past a few thousand daily writes on a shared table with heavy query traffic, it's time to at least model the split. Below that, the complexity isn't worth it.

Isn't this just CQRS?

It's in the same family. CQRS (Command Query Responsibility Segregation) is the general pattern of separating write and read models. What's described here is a pragmatic, database-level application of that idea — two tables, an async pipeline between them, no event sourcing, no separate services. You can go further (fully separate services, event-sourced write side, materialized read projections) but for most marketplaces the simpler version gets you 90% of the benefit at 20% of the complexity.

What breaks first in a logistics marketplace as supply scales?

Almost always the matching query, because it's the most index-dependent operation and it's on the hot path for every demand-side user. Onboarding latency degrades second. Ops load — support tickets about missing records, duplicate listings, stale data — climbs third and is usually the signal that leadership notices, even though the technical root cause showed up earlier.

Do we need a message queue for this, or can we use database triggers?

You can start with database triggers or a polling worker if you want to minimize infrastructure. A proper queue (Kafka, RabbitMQ, SQS, or a managed equivalent) gives you backpressure, retries, and observability that triggers do not. If your onboarding volume is bursty — and in logistics it almost always is — a queue is worth the operational overhead. If it's smooth and predictable, triggers can work for longer than people admit.

How do we handle the eventual consistency window with users?

Two things. First, make the window bounded and observable — you should know your P95 lag from write to searchable, and it should be a number you can quote. Second, be honest in the UI. Show the user their submission was received, show them when it goes live, don't pretend it's instant. Users tolerate a five-second delay they understand far better than a two-second delay that occasionally becomes thirty seconds with no explanation.

Building something in Logistics & Supply Chain?

CodeNicely partners with founders and tech teams to ship AI-native products that move metrics. Tell us about the problem you're solving.

Talk to our team