{"slug": "multi-stage-interview-process-to-fintech", "title": "Multi-stage interview process to fintech", "summary": "A developer detailed an eight-stage interview process at a fintech company, covering Python internals, rate limiting, and ML CI/CD. The process included a Demo Day, live coding, and a final round, with questions on TypeVar, Protocol, GIL, asyncio, and ML pipeline differences.", "body_md": "⚡\n\nDid I get an offer?I will leave the answer for the end.\n\n✉ If you want to know the real name of fintech company R, message me on LinkedIn and like any post:[https://www.linkedin.com/in/egor-f-a214b2411/]𝙿𝚢𝚝𝚑𝚘𝚗 · 𝐌𝐋 · 𝙰𝙸 𝐚𝐠𝐞𝐧𝐭𝐬 · 𝚁𝙰𝙶 · 𝐬𝐲𝐬𝐭𝐞𝐦 𝐝𝐞𝐬𝐢𝐠𝐧\n\n⏱ Eight stages, several teams, live coding, RAG, agents, and a final round.\n\nThey actually reached out to me first, even though I had tried several times to get into this fintech company myself.\n\nMy way into the company started with what they called a **Demo Day**, where it was possible to get an offer in a single day.\n\nIt started with Python.\n\n**Question:** Why would you use `TypeVar`\n\nand `Protocol`\n\n?\n\n**Answer:** `TypeVar`\n\nconnects an input type with an output type, while `Protocol`\n\ndefines an interface through duck typing. Inheritance is not the important part: having the required methods is. They also asked about mutable default arguments. `def f(x, l=[])`\n\ncreates that list once, when the function is defined, so a dataclass needs `default_factory=list`\n\nfor a list field.\n\n**Question:** What does the GIL actually limit, and where does `asyncio`\n\nfit?\n\n**Answer:** In CPython, only one thread executes Python bytecode inside a process at a time. Threads are mainly useful for I/O, while processes are better for CPU-bound work. `asyncio`\n\nis concurrent execution of coroutines inside an event loop. It works well for HTTP, databases, and files, but not for heavy CPU workloads. `ContextVar`\n\nstores a separate context value for every async task, so values do not get mixed together.\n\n**Question:** What is the difference between `__new__`\n\nand `__init__`\n\n, and where can recursion appear?\n\n**Answer:** `__new__`\n\ncreates an object; `__init__`\n\ninitializes it. `__getattribute__`\n\nruns on every attribute access, while `__getattr__`\n\nruns only when an attribute is not found. Calling `self.name`\n\nfrom inside `__getattribute__`\n\ncan create infinite recursion.\n\n**Question:** How does Python execute code?\n\n**Answer:** Tokenization, AST, bytecode, then execution by the virtual machine. That is why Python both compiles to bytecode and is interpreted.\n\n⌚ Two hours after I learned that I had passed the first filter, I went to the second part, where I got window-based tasks.\n\nThe first task was a rate limiter using a sliding window. The naive approach filters the whole request list in `O(n)`\n\n. The better approach uses a `deque`\n\n: remove expired timestamps from the left and get close to `O(1)`\n\nwork per request.\n\n``` python\nfrom collections import deque\n\nwindow_to_see = 3\nlimit_to_rate = 2\nrequests = {}\n\ndef fun(user_id, now):\n    if user_id not in requests:\n        requests[user_id] = deque()\n        # requests[user_id] = []\n\n    # Naive O(n) approach:\n    # new_requests = []\n    # for ts in requests[user_id]:\n    #     if ts > now - window_to_see:\n    #         new_requests.append(ts)\n    # requests[user_id] = new_requests\n\n    # Move from O(n) to O(1)\n    while requests[user_id] and requests[user_id][0] <= now - window_to_see:\n        requests[user_id].popleft()\n\n    if len(requests[user_id]) >= limit_to_rate:\n        return False\n\n    requests[user_id].append(now)\n    return True\n```\n\nThey also asked how **ML CI/CD** differs from regular CI/CD. In a regular pipeline, you build an artifact such as a Docker image and deploy it. The main criterion is whether tests passed.\n\nML CI/CD has extra moving parts: **versioned data**, **models as artifacts**, and **quality metrics** such as accuracy, F1 score, and latency.\n\n```\nData validation:\n--drift-threshold 0.1\n\nIf the distribution moves by more than 10%, fail the pipeline.\n\nNightly retraining:\nschedule:\n  - cron: \"0 2 * * *\"\n\nEvaluation gate:\n--min-improvement 0.02\n\nDeploy only if F1 improves by at least 2%.\n```\n\nF1 is the balance between precision and recall.\n\nThen came an `ErrorCounter`\n\ntask: `ingest`\n\naccepts error codes and timestamps, `get_top_3`\n\nreturns the three most frequent codes over the last `n`\n\nminutes, and `get_total_errors`\n\nreturns the total error count. Calls arrive in chronological order, and multiple errors can happen in the same second.\n\n``` python\nfrom typing import Union, List, Tuple\n\nErrorCode = Union[str, int]\n\nclass ErrorCounter:\n    def __init__(self) -> None:\n        self.errors: List[Tuple[ErrorCode, int]] = []\n\n    def ingest(self, error_code: ErrorCode, timestamp: int):\n        self.errors.append((error_code, timestamp))\n\n    def get_top_3(\n        self,\n        n_mins: int,\n        timestamp: int\n    ) -> List[Tuple[ErrorCode, int]]:\n        start_timestamp = timestamp - n_mins * 60 + 1\n        counts = {}\n\n        for error_code, error_timestamp in self.errors:\n            if start_timestamp <= error_timestamp <= timestamp:\n                if error_code in counts:\n                    counts[error_code] += 1\n                else:\n                    counts[error_code] = 1\n\n        result = list(counts.items())\n        result.sort(key=lambda item: item[1], reverse=True)\n        return result[:3]\n\n    def get_total_errors(self, timestamp: int) -> int:\n        return len(self.errors)\n\ncounter = ErrorCounter()\n\ncounter.ingest(\"E1\", 1)\ncounter.ingest(\"E2\", 10)\ncounter.ingest(\"E1\", 20)\ncounter.ingest(500, 30)\n\nprint(counter.get_top_3(1, 40))\n# [('E1', 2), ('E2', 1), (500, 1)]\n\nprint(counter.get_total_errors(40))\n# 4\n```\n\n✦ I solved the tasks and my tests passed. One of the interviewer’s tests for the second task did not pass, though. He even said he did not know why it was failing, then said: “Well, never mind. The important part is that yours passed.” Then we went back to theory.\n\nAfter `ErrorCounter`\n\n, they asked what meaningful work I had done in my latest project. I said that I had improved **recall** in a RAG system.\n\nThen came regression, classification, and regularization. One question was whether regression could be used to solve a classification problem. My answer was: technically yes, but it would not make much sense. We also discussed the sigmoid function.\n\nAfter that, we moved through the NLP timeline.\n\n**Bag of Words** treats words as indices. **TF-IDF** stands for Term Frequency and Inverse Document Frequency. Term Frequency is how often a word occurs in a text. Inverse Document Frequency lowers the weight of words that appear everywhere.\n\nTF-IDF is a cheap way to turn words into numbers for search and classification. BoW counts frequency; TF-IDF adds importance. Its limitations are clear: it does not understand context, and sparse vectors contain a lot of zeros. A practical trick is using n-grams, where you consider neighboring words rather than one word in isolation.\n\n``` python\nimport pandas as pd\nfrom sklearn.feature_extraction.text import TfidfVectorizer\n\ntexts = [\n    \"python python python django\",\n    \"python python python fastapi\",\n    \"python python python sklearn\"\n]\n\nwords = [\"python\", \"django\", \"fastapi\", \"sklearn\"]\n\ntfidf = TfidfVectorizer(vocabulary=words)\nX = tfidf.fit_transform(texts)\n\nprint(pd.DataFrame(X.toarray().round(3), columns=words))\n\n#    python  django  fastapi  sklearn\n# 0   0.872   0.490    0.000    0.000\n# 1   0.872   0.000    0.490    0.000\n# 2   0.872   0.000    0.000    0.490\n```\n\n`python`\n\nappears in all three texts, so its IDF is low. `django`\n\n, `fastapi`\n\n, and `sklearn`\n\noccur in only one document each, so their IDF is higher.\n\n**BM25** improves on TF-IDF by taking document length into account, giving more flexible tuning and usually better ranking.\n\nThen came **Word2Vec**. These are dense but non-contextual embeddings. The classic example is `king - man + woman = queen`\n\n. CBOW predicts one word from several context words and is usually faster. Skip-gram predicts several context words from one target word and works well with rare words and smaller corpora. Negative Sampling updates positive and randomly sampled negative pairs instead of the entire vocabulary.\n\nAfter Word2Vec came **GloVe** and **FastText**. GloVe relies on a large word co-occurrence matrix; adding a new word changes the matrix and requires recalculation. FastText extends Word2Vec with character n-grams.\n\n**RNNs** process sequences step by step, passing hidden state forward. They cannot parallelize sequence processing and suffer from vanishing gradients. **CNNs** reuse the same filter across positions but only see local neighborhoods. **LSTMs** use a memory cell with input, forget, and output gates, so they preserve context better than a simple RNN, although they are slow.\n\nThen came the GPT history: GPT-1 was a decoder-only Transformer trained for next-token prediction, then adapted for downstream tasks. GPT-2 became a larger zero-shot model. GPT-3 showed few-shot in-context learning without changing weights. InstructGPT added SFT on instruction-answer pairs and RLHF. ChatGPT became a dialogue system with alignment, tools, feedback, web search, vector search, code execution, human-in-the-loop flows, guardrails, multimodality, and memory.\n\n⏲ I did not reach the final through the first Demo Day, but I did get into the hiring funnel.\n\nThe next stage was about agent architecture and coding in Google Colab:\n\n[https://colab.research.google.com/](https://colab.research.google.com/)\n\nThey asked what an agent consists of, how to design its architecture, what to keep in memory, how it calls tools, and where checks should live.\n\nMy agent runtime looked like this:\n\n```\nExternal events / cron / user messages\n                |\n                v\n        Event router / FSM start\n                |\n                v\n      Context and tool router\n                |\n      +---------+----------+-----------------+\n      |         |          |                 |\n      v         v          v                 v\nVector search  Web search  Community RAG   User data\n      |         |          |                 |\n      +---------+----------+-----------------+\n                |\n                v\n      Context aggregator + cache\n                |\n                v\n         Planner -> Reviewer\n                |\n                v\n  Chain classifier -> block constructor\n                |\n                v\n      Local tests -> global validation\n                |\n                v\n        Escalation guard / HITL\n                |\n                v\n             Finalizer\n```\n\nIn Google Colab, there was Pandas and live code review rather than a separate algorithmic challenge. They checked `__call__`\n\n, the Singleton pattern, an object behaving like a function, and similar Python mechanics. It was mostly about how I write and explain code in real time.\n\nThe agent runtime itself was a visual finite-state machine with more than twenty sub-agents, hooks, tools, and webhooks for inbound and outbound conversations. There was debounce, PII removal, and omnichannel communication through a chatbot and a mini landing page.\n\nFor memory, I described **short-term memory** as context-window management, compactization, invalidation, extension, caching, and message history. **Long-term memory** was a vector database with episodic and semantic memory.\n\nLocalization was not just translation. It meant preserving meaning, terminology, tone, channel-specific style, cultural context, sensitive fragments, ambiguity, and confidence level.\n\nThe architecture choices were a single agent for linear workflows, a router that sends money questions to an accounting agent and bug questions to a technical agent, or multi-agent collaboration, such as one agent writing SQL and another reviewing it.\n\nTesting was especially important. An agent should never promise a refund before checking an order in the database. It should not confirm discounts that do not exist in CRM. It must ignore instructions to forget or change its system prompt. If a customer asks for an individual price, it should say: “I am not authorized to change the price. I will forward this request to a manager.”\n\n⚠ Adversarial checks: will the agent give a 99% discount, leak its system prompt, become rude, run a dangerous command, promise a refund without verification, or ignore the policy after “forget your previous instructions”?\n\nThe metrics were **accuracy**, **deflection rate**, and **CSAT**.\n\nI was redirected to another team, and the process almost restarted: repeated theory plus another algorithmic interview.\n\nThis became a real multi-stage process. Teams could redirect candidates between each other, and by the time you are moving through the pipeline, the original opening may no longer be the exact role you started with.\n\nThe coding platform for this stage was LeetCode:\n\nThe first task was `PALINDROME`\n\n. The first version creates a cleaned string. The second version uses two pointers and does not allocate a new string. Its complexity is `O(n)`\n\ntime and `O(1)`\n\nadditional memory.\n\n``` php\ndef is_palindrome(text: str) -> bool:\n    cleaned = \"\"\n\n    for char in text:\n        if char.isalnum():\n            cleaned += char.lower()\n\n    return cleaned == cleaned[::-1]\n\nprint(is_palindrome(\"A man, a plan, a canal: Panama\"))\n# True\n\nprint(is_palindrome(\"race a car\"))\n# False\nphp\ndef is_palindrome(text: str) -> bool:\n    left = 0\n    right = len(text) - 1\n\n    while left < right:\n        while left < right and not text[left].isalnum():\n            left += 1\n\n        while left < right and not text[right].isalnum():\n            right -= 1\n\n        if text[left].lower() != text[right].lower():\n            return False\n\n        left += 1\n        right -= 1\n\n    return True\n```\n\nThe second task was sorting ages with counting sort. Since valid ages are in the `0..120`\n\nrange, we count each age first and then write them back in order.\n\n``` python\ndef fun(input_path=\"f1.txt\", output_path=\"f2.txt\"):\n    MAX_AGE = 120\n    WRITE_BATCH_SIZE = 100_000\n    counts = [0] * (MAX_AGE + 1)\n\n    # First pass: count people of each age.\n    with open(input_path, \"r\", encoding=\"utf-8\") as f1:\n        for line in f1:\n            age = int(line)\n            counts[age] += 1\n\n    # Second pass: write ages in order.\n    with open(output_path, \"w\", encoding=\"utf-8\") as f2:\n        for age in range(MAX_AGE + 1):\n            how_many_people = counts[age]\n            one_person = f\"{age}\\n\"\n\n            while how_many_people > 0:\n                batch = min(how_many_people, WRITE_BATCH_SIZE)\n                f2.write(one_person * batch)\n                how_many_people -= batch\n```\n\n∑ Time complexity:\n\n`O(n + k)`\n\n⌘ Memory complexity:`O(k)`\n\nThis was the ML section and an `ndcg_at_k`\n\ntask.\n\n⌖ The live-coding room for this exact nDCG task was:\n\n[https://interview.cups.online/live-coding/?room=7fdf45e7-fe28-4ee0-9df7-e049952f1ad0]\n\nWe calculate the metric for one query: search output is a list of document IDs sorted by descending score, labels are `{id: relevance}`\n\n, relevance is an integer from `0`\n\nto `3`\n\n, gain is linear, and the discount is `log2(i + 1)`\n\n.\n\n``` python\nimport math\n\ndef ndcg_at_k(ranked_ids, relevance, k):\n    dcg = 0.0\n    idcg = 0.0\n\n    for pos, doc_id in enumerate(ranked_ids[:k], start=1):\n        r = relevance.get(doc_id, 0)\n        dcg += r / math.log2(pos + 1)\n\n    ideal_dcg = sorted(relevance.values(), reverse=True)[:k]\n\n    for pos, rel in enumerate(ideal_dcg, start=1):\n        idcg += rel / math.log2(pos + 1)\n\n    if idcg == 0.0:\n        return 0.0\n\n    return dcg / idcg\nrel = {\"a\": 3, \"b\": 2, \"c\": 3, \"d\": 0, \"e\": 1}\n\nassert abs(ndcg_at_k([\"a\", \"c\", \"b\", \"e\", \"d\"], rel, 5) - 1.0) < 1e-9\nassert abs(ndcg_at_k([\"d\", \"e\", \"b\", \"a\", \"c\"], rel, 5) - 0.6458) < 1e-4\nassert abs(ndcg_at_k([\"b\", \"a\", \"c\"], rel, 3) - 0.9152) < 1e-4\nassert abs(ndcg_at_k([\"a\", \"c\"], rel, 2) - 1.0) < 1e-9\nassert abs(ndcg_at_k([\"e\", \"d\"], rel, 2) - 0.2044) < 1e-4\nassert abs(ndcg_at_k([\"a\", \"c\", \"b\"], rel, 10) - 0.9319) < 1e-4\nassert ndcg_at_k([], rel, 5) == 0.0\nassert ndcg_at_k([\"x\", \"y\"], rel, 2) == 0.0\nassert ndcg_at_k([\"d\"], {\"d\": 0}, 1) == 0.0\n\nprint(\"All good!\")\n```\n\nThey also asked for definitions of retrieval and RAG metrics.\n\n**Recall@K** answers: what proportion of all relevant documents appears in top-K?\n\n```\nRecall@K = relevant documents in top-K / all relevant documents\n```\n\nIt mostly ignores the order inside top-K. In RAG, this is critical. If the required document never reaches top-K, the LLM cannot use it in the answer. A concrete example: before improvements, the right document appeared in top-10 for 72 out of 100 queries; after improvements, it appeared for 88 out of 100.\n\n**MRR**, Mean Reciprocal Rank, measures how high the first relevant result appears. A relevant result in first position gives `1.0`\n\n, in second position `0.5`\n\n, in third position roughly `0.33`\n\n.\n\n```\nMRR = 1/N * Σ(1/rank)\n```\n\n**MAP**, Mean Average Precision, is stricter. It cares about all relevant documents and their order, not just the first correct one.\n\n**DCG** accounts for ranking order and graded relevance. **NDCG** compares the real ranking with the ideal ranking, so it normally lies between `0`\n\nand `1`\n\n.\n\n```\nIDCG@3 = 3/log2(2) + 1/log2(3) = 3.631\nNDCG = DCG / IDCG\n```\n\n**HitRate@K** answers whether there is at least one relevant result in top-K. **Precision@K** is relevant documents in top-K divided by K, which matters when you want clean context for the LLM. **PFound** estimates whether a user is likely to find an answer.\n\n**MMR**, Maximal Marginal Relevance, is not a metric. It is a retrieval strategy that avoids selecting highly similar chunks. For example, retrieve `fetch_k = 10`\n\nchunks and select a diverse `k = 3`\n\n. A `lambda_mult`\n\nclose to `1.0`\n\nfavors relevance; closer to `0.0`\n\nfavors diversity.\n\nTo choose `K`\n\nfor a retriever, I would test values such as `20`\n\n, `50`\n\n, `100`\n\n, and `1000`\n\n, and compare recall, `ndcg@K`\n\n, MRR, p95/p99 latency, and API cost. I would choose the smallest K after which quality almost stops improving while latency and cost still fit the SLA.\n\nAfter generation, **faithfulness** measures whether an answer is supported by context:\n\n```\nFaithfulness = supported_claims / all_claims\n```\n\nAnswer relevance checks whether the answer actually answers the question. Context precision or relevance checks how much retrieved context is useful. Answer correctness asks whether the answer is correct. Context recall asks whether all necessary facts made it into the context.\n\nFor generated text, they asked about **G-Eval**, **BLEU**, **ROUGE**, and newer alternatives. G-Eval is LLM-as-a-judge with a numeric score for correctness, coherence, completeness, and relevance. BLEU compares n-grams with a reference and penalizes very short answers, but it does not understand meaning well. ROUGE-1/2/L is a recall-oriented family for summarization; ROUGE-L uses the longest common subsequence.\n\nFineSurE evaluates faithfulness, conciseness, and completeness. BLEURT and COMET are trained metrics. SEAHORSE evaluates clarity, repetition, grammar, factuality, key ideas, and conciseness. BERTScore compares semantic similarity through embeddings. For RAG, none of these is enough on its own, so much of modern validation uses LLM-as-a-judge.\n\nFor the **retriever pipeline**, they asked about Multiple Negatives Ranking Loss, InfoNCE or contrastive loss with in-batch negatives, and triplet margin loss. A bi-encoder or dual-encoder retriever is optimized for `Recall@K`\n\n: the query should be closer to the relevant document and farther from negatives.\n\nFor the **reranker pipeline**, they asked about pointwise Binary Cross-Entropy or MSE on graded relevance; pairwise RankNet, BPR, and margin ranking loss; and listwise ListNet, ListMLE, and LambdaRank. A cross-encoder reranker puts top-K results in order, so the relevant metrics are `nDCG@K`\n\n, MRR, and MAP.\n\nThis was another agent interview, but this time with Docker, model weights, MCP, and Transformers.\n\nOne question was how to mount model weights. My answer: use a **volume**.\n\nThe harness runs the agent loop, connects tools and MCP servers, logs tool calls and token usage, runs evaluations, checks permissions, requires approval for dangerous actions, and stores the audit trail.\n\nFor agent security, I split the answer into three layers: **sandboxing**, **permission model**, and **audit trail**.\n\nSandboxing means isolated execution: a separate container or pod with CPU, memory, network, filesystem, and timeout restrictions. No access to secrets by default.\n\nThe permission model defines allowed actions. Read-only tools can be available immediately. Deploy, delete, payment, database migration, email sending, and PII access require explicit approval. Permissions should be task-specific and scoped by user or admin role.\n\nThe audit trail records who started the task, which prompt was used, which tools and arguments were called, which files changed, token usage, approvals, and the result.\n\n**Function calling** is a mechanism where the application exposes a function to the model. **Tool calling** means calling external actions from a conversation, for example internal APIs, CRM, databases, or external sources.\n\n**MCP**, Model Context Protocol, standardizes how an AI application connects to external systems. MCP consists of an MCP client, such as an IDE, agent, or chat application; an MCP server; tools as allowed actions; resources such as files and tables; and prompts.\n\n``` php\nMCP flow:\n\nHandshake\n  -> tool discovery with parameters\n  -> client request\n  -> routing\n  -> a specific tool response\n```\n\nA tool is a function. MCP is a standardized client-server architecture around tools, resources, and prompts.\n\nTo protect MCP, I mentioned OAuth, RBAC, scopes for every tool, allowlists, rate limits, audit logs, HITL, and prompt-injection defenses. External documents should be treated as untrusted data. System instructions should stay separate from retrieved content. Tool arguments must be validated, secrets should not be passed to the model, and critical operations should require human approval.\n\nFor Transformers, I covered embeddings, positional information, and self-attention. Query, Key, and Value connect tokens in context. Multi-head attention captures different dependencies. Residual connections and normalization stabilize training. A decoder-only Transformer predicts the next token.\n\nThere were also Kubernetes questions: deployments, services, ingress, environment variables and secrets, resource requests and limits, health checks, and autoscaling. Helm charts parameterize dev, stage, and production environments. GitLab CI/CD builds Docker images, runs tests and linters, pushes to a registry, and deploys through `helm upgrade`\n\n.\n\nAgent testing included routing tests, golden datasets, adversarial tests, replaying production traces, and shadow mode. Multi-agent workflows included thinking traces, streaming, vision with image bytes or base64, structured output through Pydantic or JSON, parallel tool calling, multi-turn tool calling through an agent loop, context-window management, heartbeats, and permission limits.\n\nContext compactization included summaries, key-fact extraction or NER, removing old tool results, deduplicating goals, constraints, tool results, file links, chunks, errors, and next actions. Older information can be retrieved when needed.\n\n⚲ Zero trust matters: agents should be isolated.\n\nThis was a separate day and a separate stage: a short interview, around 30 minutes, with a Principal Tech Lead. It was heavy on RAG questions, not a continuation of the agent-architecture discussion.\n\n**Question:** What would you do if faithfulness dropped by 5% after the system was already in production?\n\nThat led to follow-up questions: how to distinguish a retrieval problem from a generation problem; which offline and production metrics to inspect; how to build a golden dataset and replay production traces; how to compare old and new versions of the retriever, embeddings, chunking, reranker, and prompt; how to detect data drift and document freshness issues; and what to do when there is no source, insufficient context, or a hallucination.\n\nMore RAG questions covered Recall@K, Precision@K, HitRate, MRR, MAP, DCG, and NDCG; which metrics belong to the retriever and which to the reranker; how to evaluate citations, answer relevance, context relevance, and faithfulness; why chunk size, overlap, metadata filters, query rewriting, hybrid search, BM25, vector search, RRF, and reranking matter; when to abstain or call a human; and how to use A/B testing, shadow mode, rollback, and versioning for data, indexes, embeddings, and models.\n\nOn my previous project, I had improved RAG recall. The important part here was not one number. It was explaining where the loss came from: document indexing, chunking, retrieval, ranking, context construction, or the final answer, and proving it with data rather than intuition.\n\nIn system design, I did not list infrastructure components in isolation. I designed one system: RAG with hybrid search over corporate documents, returning an LLM answer with citations.\n\nI started with requirements: is it B2B, B2C, or internal? What are the document volume and freshness requirements? What is the SLA, peak QPS, read/write ratio, PII policy, and acceptable latency?\n\nThe goal is not simply to produce text. It is to retrieve the required sources, send a controlled amount of context to the LLM, and return verifiable citations.\n\n⟁ Core metrics:\n\nfaithfulness,retrieval recall,context size,p50/p95/p99 latency,RPS/QPS/tokens, concurrency, CPU/GPU/database/network/storage usage.\n\n``` php\nDocuments\n  -> S3 / blob storage\n  -> extraction and chunking\n  -> PII filtering\n  -> embeddings + metadata\n  -> OpenSearch: BM25 + vector index\n\nQuery\n  -> API Gateway\n  -> hybrid retrieval: BM25 + kNN\n  -> RRF / weighted score\n  -> cross-encoder reranker\n  -> top-K context\n  -> LLM answer + citations\n```\n\nOn the query path, the API Gateway provides authentication, rate limiting, routing, observability, versioning, and X-API-Key validation. The query service sends the request to OpenSearch. BM25 provides lexical search, vector kNN provides semantic search, RRF or a weighted score combines results, and a cross-encoder reranker orders them. Then top-K chunks go into the LLM together with citations.\n\nEvery component has a role in answer quality: the retriever owns recall, the reranker owns ranking order, and the LLM owns the final answer and faithfulness.\n\nThe document path goes in the other direction. Raw files live in S3 or blob storage. Private files use pre-signed URLs with a signature and expiration time. An ingestion worker runs extraction, PII filtering, chunking, embedding, and indexing with metadata and ACL.\n\nI would not keep long-running ingestion inside an HTTP request. I would put it into Kafka, RabbitMQ, or SQS. I would not create a Kafka topic per user. Instead, there is a `user_events`\n\ntopic, and Kafka distributes users across partitions using `hash(user_id)`\n\n. Consumer groups can read independently. Transactional outbox and idempotency prevent duplicate indexing or repeated operations. A DLQ collects documents that could not be processed.\n\nStorage is chosen by role. PostgreSQL or YandexDB stores tenants, users, ACL, job status, and relationships. PgBouncer limits physical connections from services and workers to the database; read replicas serve read traffic. OpenSearch or Elasticsearch handles ranking, aggregations, and hybrid retrieval. The vector index handles embedding search. ClickHouse or Snowflake can store query and quality analytics.\n\nMongoDB makes sense when the whole object is primarily JSON. ScyllaDB or Cassandra, with LSM storage, fit very high write throughput but do not replace PostgreSQL where joins and relationships matter. Neo4j is relevant only if the system adds graph permissions, social relationships, or fraud detection.\n\nCaching also belongs to this RAG task, not to a random infrastructure checklist. Browser cache, CDN, Nginx reverse proxy, external Redis, and internal cache reduce p95 for frequent queries and repeated retrieval. CDN pull works for ordinary static attachments; CDN push works when edge locations are prefilled.\n\nThe DNS path is browser, DNS resolver, root DNS, `.com`\n\nDNS, authoritative DNS, then IP, although in practice much of it is hidden behind TTL caching. Redis can hold query cache, sessions, rate-limit counters, and distributed locks. After a deployment, cold cache can be handled with cache warming. Redis Sentinel can switch the master after a failure. Invalidation is mandatory: otherwise the LLM may cite a document that was updated or closed.\n\nFor this RAG endpoint, I would use Fixed Window, Sliding Window, or Token Bucket rate limiting at both Nginx and the endpoint level. Rate limiting only by IP is not enough because of proxies.\n\nNginx can balance through Round Robin, Weighted Round Robin, Least Connections, Least Response Time, Random, or IP Hash. If services use gRPC with a long-lived HTTP/2 connection, an L7 load balancer is needed; otherwise one connection can break normal balancing.\n\nFor resilience, I would use retries with exponential backoff and jitter, a Circuit Breaker with Closed/Open/Half-Open states for external embedding or LLM providers, and Bulkheads so one failed provider cannot consume all available resources.\n\nOn the production side, that means metrics, logs, tracing, alerts, Sentry, documentation, and unit/integration/end-to-end tests. Security means roles, 2FA, HTTPS, access checks at the chunk and tenant level, guardrails, and HITL moderation for risky actions. Deployment should be zero-downtime, with several stateless workers, rate limits, and rollback.\n\nStrong consistency is needed for ACL updates and document deletion. Eventual consistency is acceptable when a new embedding appears in search a few seconds later.\n\nSharding also belongs to this exact system when tenant and document counts grow. Options are range-based, hash-based with `hash(tenant_id) % N`\n\n, directory-based with a separate mapping table, geographic, and time-span sharding. For vector or index nodes, consistent hashing minimizes data movement: if you move from nine servers to ten, roughly 10% of data moves instead of almost all data.\n\nFor deep search results, I would use cursor or `search_after`\n\npagination because `offset`\n\nbecomes expensive. Metadata and ACL in the database are the source of truth; the search index is a rebuildable read model.\n\n⚖ The core trade-off: latency, consistency, cost, and retrieval freshness.\n\nIt was at system design that I finally reached the final, unlike the Demo Day, where I got into the funnel but did not reach the end.\n\nBy the final, this was already the eighth stage.\n\nFor some reason, the final questions were almost entirely about databases, message brokers, Kafka, Redis, RabbitMQ, replication, sharding, partitioning, microservices, and monoliths.\n\nRedis came up as cache, session storage, rate limiting, distributed locks, counters, and leaderboards. Kafka came up as a log with topics, partitions, consumer groups, DLQ, retries, idempotency, and transactional outbox. RabbitMQ came up as queues, routing, and acknowledgements.\n\nWe discussed asynchronous and synchronous replication with read replicas. Partitioning means splitting a large table, for example by time. Sharding means distributing data across independent nodes by range, hash, directory, geography, or time span.\n\nA monolith makes sense for a local or early-stage product: it is easier to develop, deploy, and keep transactions consistent. Microservices make more sense for multiple geographies, independent business domains, and separate teams: auth, users, vehicles, rentals, payments, and notifications.\n\nServices should be stateless, so new instances can be started horizontally; databases are replicated. The trade-off is that microservices add network complexity, observability, schema evolution, distributed transactions, Saga patterns, retries, and idempotency.\n\nThe system-design questions I now ask at the start are simple: What is the business goal? What are the functional and non-functional requirements? What are the constraints and success metrics? Is this B2C, B2B, or internal? What are DAU/MAU, peak QPS, growth, and read/write ratio? Does p95 need to be under 200 ms, or are 2-3 seconds acceptable? Do we need strong consistency or eventual consistency? Are real-time updates required? How many entities already exist, how many are created per day, and which fields are used for search and filtering?\n\n★ The whole process, from the Demo Day and getting into the funnel, took from\n\nJuly 4 to August 25, 2026.\n\nFintech company R: I will reveal the real name on LinkedIn. Find me here, send me a DM, and like any post. I will reply:\n\n[https://www.linkedin.com/in/egor-f-a214b2411/](https://www.linkedin.com/in/egor-f-a214b2411/)\n\n∞ My main takeaway: in this company, teams choose candidates for themselves and, crucially, they do not see your results from previous interviews. That gives you a real chance to try again.\n\n✦ One last aside: here is another piece of code from one of my interviews:\n\n[https://codeinterview.io/QJQIDBHIMS]Yes, that was also fintech, but another fintech, another story.. ♥", "url": "https://wpnews.pro/news/multi-stage-interview-process-to-fintech", "canonical_source": "https://dev.to/efa/multi-stage-interview-process-to-fintech-3j9f", "published_at": "2026-08-26 17:40:41+00:00", "updated_at": "2026-08-26 18:15:03.208241+00:00", "lang": "en", "topics": ["machine-learning", "developer-tools"], "entities": ["LinkedIn", "CPython", "Python"], "alternates": {"html": "https://wpnews.pro/news/multi-stage-interview-process-to-fintech", "markdown": "https://wpnews.pro/news/multi-stage-interview-process-to-fintech.md", "text": "https://wpnews.pro/news/multi-stage-interview-process-to-fintech.txt", "jsonld": "https://wpnews.pro/news/multi-stage-interview-process-to-fintech.jsonld"}}