β‘
Did I get an offer?I will leave the answer for the end.
β 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/]πΏπ’ππππ Β· ππ Β· π°πΈ ππ ππ§ππ¬ Β· ππ°πΆ Β· π¬π²π¬πππ¦ πππ¬π’π π§
β± Eight stages, several teams, live coding, RAG, agents, and a final round.
They actually reached out to me first, even though I had tried several times to get into this fintech company myself.
My way into the company started with what they called a Demo Day, where it was possible to get an offer in a single day.
It started with Python.
Question: Why would you use TypeVar
and Protocol
?
Answer: TypeVar
connects an input type with an output type, while Protocol
defines 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=[])
creates that list once, when the function is defined, so a dataclass needs default_factory=list
for a list field.
Question: What does the GIL actually limit, and where does asyncio
fit?
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
is concurrent execution of coroutines inside an event loop. It works well for HTTP, databases, and files, but not for heavy CPU workloads. ContextVar
stores a separate context value for every async task, so values do not get mixed together.
Question: What is the difference between __new__
and __init__
, and where can recursion appear?
Answer: __new__
creates an object; __init__
initializes it. __getattribute__
runs on every attribute access, while __getattr__
runs only when an attribute is not found. Calling self.name
from inside __getattribute__
can create infinite recursion.
Question: How does Python execute code?
Answer: Tokenization, AST, bytecode, then execution by the virtual machine. That is why Python both compiles to bytecode and is interpreted.
β Two hours after I learned that I had passed the first filter, I went to the second part, where I got window-based tasks.
The first task was a rate limiter using a sliding window. The naive approach filters the whole request list in O(n)
. The better approach uses a deque
: remove expired timestamps from the left and get close to O(1)
work per request.
from collections import deque
window_to_see = 3
limit_to_rate = 2
requests = {}
def fun(user_id, now):
if user_id not in requests:
requests[user_id] = deque()
while requests[user_id] and requests[user_id][0] <= now - window_to_see:
requests[user_id].popleft()
if len(requests[user_id]) >= limit_to_rate:
return False
requests[user_id].append(now)
return True
They 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.
ML CI/CD has extra moving parts: versioned data, models as artifacts, and quality metrics such as accuracy, F1 score, and latency.
Data validation:
--drift-threshold 0.1
If the distribution moves by more than 10%, fail the pipeline.
Nightly retraining:
schedule:
- cron: "0 2 * * *"
Evaluation gate:
--min-improvement 0.02
Deploy only if F1 improves by at least 2%.
F1 is the balance between precision and recall.
Then came an ErrorCounter
task: ingest
accepts error codes and timestamps, get_top_3
returns the three most frequent codes over the last n
minutes, and get_total_errors
returns the total error count. Calls arrive in chronological order, and multiple errors can happen in the same second.
from typing import Union, List, Tuple
ErrorCode = Union[str, int]
class ErrorCounter:
def __init__(self) -> None:
self.errors: List[Tuple[ErrorCode, int]] = []
def ingest(self, error_code: ErrorCode, timestamp: int):
self.errors.append((error_code, timestamp))
def get_top_3(
self,
n_mins: int,
timestamp: int
) -> List[Tuple[ErrorCode, int]]:
start_timestamp = timestamp - n_mins * 60 + 1
counts = {}
for error_code, error_timestamp in self.errors:
if start_timestamp <= error_timestamp <= timestamp:
if error_code in counts:
counts[error_code] += 1
else:
counts[error_code] = 1
result = list(counts.items())
result.sort(key=lambda item: item[1], reverse=True)
return result[:3]
def get_total_errors(self, timestamp: int) -> int:
return len(self.errors)
counter = ErrorCounter()
counter.ingest("E1", 1)
counter.ingest("E2", 10)
counter.ingest("E1", 20)
counter.ingest(500, 30)
print(counter.get_top_3(1, 40))
print(counter.get_total_errors(40))
β¦ 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.
After ErrorCounter
, they asked what meaningful work I had done in my latest project. I said that I had improved recall in a RAG system.
Then 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.
After that, we moved through the NLP timeline.
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.
TF-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.
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
texts = [
"python python python django",
"python python python fastapi",
"python python python sklearn"
]
words = ["python", "django", "fastapi", "sklearn"]
tfidf = TfidfVectorizer(vocabulary=words)
X = tfidf.fit_transform(texts)
print(pd.DataFrame(X.toarray().round(3), columns=words))
python
appears in all three texts, so its IDF is low. django
, fastapi
, and sklearn
occur in only one document each, so their IDF is higher.
BM25 improves on TF-IDF by taking document length into account, giving more flexible tuning and usually better ranking.
Then came Word2Vec. These are dense but non-contextual embeddings. The classic example is king - man + woman = queen
. 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.
After 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.
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.
Then 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.
β² I did not reach the final through the first Demo Day, but I did get into the hiring funnel.
The next stage was about agent architecture and coding in Google Colab:
https://colab.research.google.com/
They 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.
My agent runtime looked like this:
External events / cron / user messages
|
v
Event router / FSM start
|
v
Context and tool router
|
+---------+----------+-----------------+
| | | |
v v v v
Vector search Web search Community RAG User data
| | | |
+---------+----------+-----------------+
|
v
Context aggregator + cache
|
v
Planner -> Reviewer
|
v
Chain classifier -> block constructor
|
v
Local tests -> global validation
|
v
Escalation guard / HITL
|
v
Finalizer
In Google Colab, there was Pandas and live code review rather than a separate algorithmic challenge. They checked __call__
, 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.
The 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.
For 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.
Localization was not just translation. It meant preserving meaning, terminology, tone, channel-specific style, cultural context, sensitive fragments, ambiguity, and confidence level.
The 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.
Testing 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.β
β 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β?
The metrics were accuracy, deflection rate, and CSAT.
I was redirected to another team, and the process almost restarted: repeated theory plus another algorithmic interview.
This 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.
The coding platform for this stage was LeetCode:
The first task was PALINDROME
. 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)
time and O(1)
additional memory.
def is_palindrome(text: str) -> bool:
cleaned = ""
for char in text:
if char.isalnum():
cleaned += char.lower()
return cleaned == cleaned[::-1]
print(is_palindrome("A man, a plan, a canal: Panama"))
print(is_palindrome("race a car"))
php
def is_palindrome(text: str) -> bool:
left = 0
right = len(text) - 1
while left < right:
while left < right and not text[left].isalnum():
left += 1
while left < right and not text[right].isalnum():
right -= 1
if text[left].lower() != text[right].lower():
return False
left += 1
right -= 1
return True
The second task was sorting ages with counting sort. Since valid ages are in the 0..120
range, we count each age first and then write them back in order.
def fun(input_path="f1.txt", output_path="f2.txt"):
MAX_AGE = 120
WRITE_BATCH_SIZE = 100_000
counts = [0] * (MAX_AGE + 1)
with open(input_path, "r", encoding="utf-8") as f1:
for line in f1:
age = int(line)
counts[age] += 1
with open(output_path, "w", encoding="utf-8") as f2:
for age in range(MAX_AGE + 1):
how_many_people = counts[age]
one_person = f"{age}\n"
while how_many_people > 0:
batch = min(how_many_people, WRITE_BATCH_SIZE)
f2.write(one_person * batch)
how_many_people -= batch
β Time complexity:
O(n + k)
β Memory complexity:O(k)
This was the ML section and an ndcg_at_k
task.
β The live-coding room for this exact nDCG task was:
[https://interview.cups.online/live-coding/?room=7fdf45e7-fe28-4ee0-9df7-e049952f1ad0]
We calculate the metric for one query: search output is a list of document IDs sorted by descending score, labels are {id: relevance}
, relevance is an integer from 0
to 3
, gain is linear, and the discount is log2(i + 1)
.
import math
def ndcg_at_k(ranked_ids, relevance, k):
dcg = 0.0
idcg = 0.0
for pos, doc_id in enumerate(ranked_ids[:k], start=1):
r = relevance.get(doc_id, 0)
dcg += r / math.log2(pos + 1)
ideal_dcg = sorted(relevance.values(), reverse=True)[:k]
for pos, rel in enumerate(ideal_dcg, start=1):
idcg += rel / math.log2(pos + 1)
if idcg == 0.0:
return 0.0
return dcg / idcg
rel = {"a": 3, "b": 2, "c": 3, "d": 0, "e": 1}
assert abs(ndcg_at_k(["a", "c", "b", "e", "d"], rel, 5) - 1.0) < 1e-9
assert abs(ndcg_at_k(["d", "e", "b", "a", "c"], rel, 5) - 0.6458) < 1e-4
assert abs(ndcg_at_k(["b", "a", "c"], rel, 3) - 0.9152) < 1e-4
assert abs(ndcg_at_k(["a", "c"], rel, 2) - 1.0) < 1e-9
assert abs(ndcg_at_k(["e", "d"], rel, 2) - 0.2044) < 1e-4
assert abs(ndcg_at_k(["a", "c", "b"], rel, 10) - 0.9319) < 1e-4
assert ndcg_at_k([], rel, 5) == 0.0
assert ndcg_at_k(["x", "y"], rel, 2) == 0.0
assert ndcg_at_k(["d"], {"d": 0}, 1) == 0.0
print("All good!")
They also asked for definitions of retrieval and RAG metrics.
Recall@K answers: what proportion of all relevant documents appears in top-K?
Recall@K = relevant documents in top-K / all relevant documents
It 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.
MRR, Mean Reciprocal Rank, measures how high the first relevant result appears. A relevant result in first position gives 1.0
, in second position 0.5
, in third position roughly 0.33
.
MRR = 1/N * Ξ£(1/rank)
MAP, Mean Average Precision, is stricter. It cares about all relevant documents and their order, not just the first correct one.
DCG accounts for ranking order and graded relevance. NDCG compares the real ranking with the ideal ranking, so it normally lies between 0
and 1
.
IDCG@3 = 3/log2(2) + 1/log2(3) = 3.631
NDCG = DCG / IDCG
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.
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
chunks and select a diverse k = 3
. A lambda_mult
close to 1.0
favors relevance; closer to 0.0
favors diversity.
To choose K
for a retriever, I would test values such as 20
, 50
, 100
, and 1000
, and compare recall, ndcg@K
, 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.
After generation, faithfulness measures whether an answer is supported by context:
Faithfulness = supported_claims / all_claims
Answer 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.
For 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.
FineSurE 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.
For 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
: the query should be closer to the relevant document and farther from negatives.
For 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
, MRR, and MAP.
This was another agent interview, but this time with Docker, model weights, MCP, and Transformers.
One question was how to mount model weights. My answer: use a volume.
The 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.
For agent security, I split the answer into three layers: sandboxing, permission model, and audit trail.
Sandboxing means isolated execution: a separate container or pod with CPU, memory, network, filesystem, and timeout restrictions. No access to secrets by default.
The 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.
The 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.
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.
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.
MCP flow:
Handshake
-> tool discovery with parameters
-> client request
-> routing
-> a specific tool response
A tool is a function. MCP is a standardized client-server architecture around tools, resources, and prompts.
To 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.
For 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.
There 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
.
Agent 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.
Context 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.
β² Zero trust matters: agents should be isolated.
This 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.
Question: What would you do if faithfulness dropped by 5% after the system was already in production?
That 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.
More 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.
On 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.
In 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.
I 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?
The 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.
β Core metrics:
faithfulness,retrieval recall,context size,p50/p95/p99 latency,RPS/QPS/tokens, concurrency, CPU/GPU/database/network/storage usage.
Documents
-> S3 / blob storage
-> extraction and chunking
-> PII filtering
-> embeddings + metadata
-> OpenSearch: BM25 + vector index
Query
-> API Gateway
-> hybrid retrieval: BM25 + kNN
-> RRF / weighted score
-> cross-encoder reranker
-> top-K context
-> LLM answer + citations
On 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.
Every 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.
The 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.
I 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
topic, and Kafka distributes users across partitions using hash(user_id)
. Consumer groups can read independently. Transactional outbox and idempotency prevent duplicate indexing or repeated operations. A DLQ collects documents that could not be processed.
Storage 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.
MongoDB 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.
Caching 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.
The DNS path is browser, DNS resolver, root DNS, .com
DNS, 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.
For 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.
Nginx 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.
For 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.
On 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.
Strong consistency is needed for ACL updates and document deletion. Eventual consistency is acceptable when a new embedding appears in search a few seconds later.
Sharding also belongs to this exact system when tenant and document counts grow. Options are range-based, hash-based with hash(tenant_id) % 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.
For deep search results, I would use cursor or search_after
pagination because offset
becomes expensive. Metadata and ACL in the database are the source of truth; the search index is a rebuildable read model.
β The core trade-off: latency, consistency, cost, and retrieval freshness.
It 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.
By the final, this was already the eighth stage.
For some reason, the final questions were almost entirely about databases, message brokers, Kafka, Redis, RabbitMQ, replication, sharding, partitioning, microservices, and monoliths.
Redis 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.
We 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.
A 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.
Services 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.
The 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?
β The whole process, from the Demo Day and getting into the funnel, took from
July 4 to August 25, 2026.
Fintech company R: I will reveal the real name on LinkedIn. Find me here, send me a DM, and like any post. I will reply:
https://www.linkedin.com/in/egor-f-a214b2411/
β 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.
β¦ One last aside: here is another piece of code from one of my interviews:
[https://codeinterview.io/QJQIDBHIMS]Yes, that was also fintech, but another fintech, another story.. β₯