Skip to content
View PModhe09's full-sized avatar

Block or report PModhe09

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Content in all repositories owned by your account will be closed.
Maximum 250 characters. Please don’t include any personal information such as legal names or email addresses. Markdown is supported. This note will only be visible to you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
PModhe09/README.md

Prathmesh Modhe

Software Development Engineer · Reelo · Ahmedabad, India
Focus: backend · data at scale · applied AI

I build and own systems end to end: APIs and data platforms, LLM tooling that runs in front of real customers,
and the migrations and debugging that keep them working once they ship.


Applied AI

MCP Agentic Orchestration Tool Calling Prompt Engineering LLM Observability Vector Search Bedrock Qdrant

Backend & Data

Node.js Express Python FastAPI ClickHouse MongoDB Redis SQL BullMQ

Distributed Systems & Cloud

EventBridge ECS Fargate SQS Lambda S3 Docker Datadog

Languages

JavaScript Python SQL C++


What I do

Most of my code is in private repos, so here is what I have built and the parts that were hard.

🧠 LLM systems in production A Python MCP server that exposes our analytics as tools, behind a proxy that checks permissions first, so a tool call can never read data the user isn't allowed to see. An orchestrator across 28 modules that picks tools, builds the prompt, and runs a judge over the answer to catch weak responses before they reach anyone. A monthly report where the model writes the words and the code computes every number.

⚡ Query performance Made scans 83% faster on a 50M+ row ClickHouse table by changing the sort key to match how we actually query it. Another query got 70x faster by swapping a filter the index couldn't use for a plain date range. Exports now stream instead of loading whole result sets into memory.

🗄️ The data platform 60+ REST endpoints over ClickHouse and MongoDB, feeding six products: dashboards, a daily summary, a monthly report, an AI assistant, notifications and fraud alerts. I own this layer end to end, including what other teams build on top of it.

🧱 Rebuilding the pipeline underneath Rebuilt the pre-aggregated rollup table that almost everything reads from, adding new fields across both MongoDB and ClickHouse and refactoring the service layer around it. When two stores disagreed on the same number, it was usually here.

🚢 Shipping without breaking things Changed how every order is dated, across ingestion, metrics and every downstream query. Five phases, each safe to deploy on its own, behind a feature flag, ~50 files, and nothing broke for existing stores. Old data was backfilled with scripts that could be re-run safely.

🏗️ Design that held up A builder pattern for store comparison that took 12 new metrics over two months without the core being rewritten. Threshold classes instead of branching in the segment engine, so adding a rule meant adding a class.

🔀 Distributed systems Built the scheduling layer that fans work out across EventBridge Scheduler, an ECS Fargate task queue and SQS: per-user schedules in each user's own timezone, tasks queued against Fargate capacity, results fanned out per recipient. A signal engine on top picks the one insight worth sending each night, and sends nothing when there isn't one.

🔍 Debugging under load Found 500s and 502s were memory pressure from buffering big result sets. Traced a MongoDB CPU spike to a background job with no index, which showed no slow endpoint at all. Traced an API latency spike to a dead Redis connection: every cache miss fell through to MongoDB and piled up.

Also ship React and Redux across the product's dashboards when a feature needs it end to end.


How some of it actually works (for the curious)

Picking tools without asking the model

We have 17 tools. The normal way is to send every tool description to the model and let it choose, which means every question pays for all 17 descriptions, and it gets worse each time you add one.

So the model doesn't do that step. Tool descriptions are embedded in Qdrant, the question gets embedded when it arrives, and only the closest two go to the model. Nothing is spent on deciding.

Similarity doesn't understand intent though. "How much did I make" kept returning top-selling-items, because the words look like an items question. Rather than fight the embeddings, there are plain rules for the few intents that actually got it wrong in practice.

A judge that isn't allowed to give hints

After the model answers, a judge checks it against the question and sends feedback back for one retry. It can say what's missing, but it cannot name a tool or a parameter, so the model has to choose again rather than be told. Tool rounds and retries are both capped, because a loop with no limit means a bill with no limit.

Keeping the model away from the numbers

The monthly report is written by a model, but every figure is calculated in code first. The output shape is locked by a tool call with a fixed schema, and a guard drops anything the model made up or wandered into.

The reason is simple. This report goes to a business owner about their own revenue. One confident wrong sentence and they stop trusting the whole thing, and they'd be right to.

Knowing when to say nothing

The nightly engine looks at each store, ranks what happened, and sends at most one thing. The harder half is silence. If no customers were tagged, telling someone their capture rate is 0% is useless. If the store was closed, sales look like they collapsed. And it remembers what it sent last time, so the same message doesn't turn up three nights running.

Filtering on a column the table isn't sorted by

The business-hours work added a business_at column, but the table is sorted on created_at. Filtering on business_at alone gives the right answer slowly, because the database can no longer skip partitions.

So there are two filters. A padded created_at range in PREWHERE does the skipping, wide enough that no order crossing midnight gets dropped. An exact business_at check in WHERE does the correctness, falling back to created_at for rows written before the change.

Settings that remember what they used to be

Business hours change. If a store moves its closing time, last quarter's report still has to use the hours that were in force back then, not today's. So hours are stored with effective_from, effective_to and a version, and each query looks up the window for the date it's reporting on.

A builder that took twelve metrics

Store comparison needed sales, orders, AOV, APC, dropoff, tagging, frequency, time between orders, day-wise breakup, redemptions and new-vs-repeat, each split by store. Twelve near-identical endpoints was the obvious route.

The builder went in on day one, then got reshaped on day two once the second metric showed where it was wrong. After that a metric landed roughly every week for two months and the core never changed. Inactive stores stay visible in comparisons through an all_stores CTE with LEFT JOINs, instead of quietly disappearing.


Open to backend, platform and applied AI roles. Easiest to reach on LinkedIn or at pmodhe19@gmail.com.

Pinned Loading

  1. Vibe Vibe Public

    JavaScript