DrawLintDrawLint.ai

Design an OTP generation and Verification system — system design by AgileViper46

Hire

Reviewed by 6 specialized AI reviewers. Explore the diagram and the full per-section feedback below.

Loading diagram…

OTP generation flow: Clients send the request on the /otp/send endpoint with the information about the user, how they want to be commmunicated, and type of the otp (short, medium, long - 1m, 5m, 10m) The otp service takes this request and first checks in the idempotency table if the same otp was created or not. If created send the otpId which was created for it with the status of it. in case of not created, first the otp service will generate the otp the otp will be generated by a random nounce (secured with client secret) + timestamp + client id and the hashing over it (hashing pipeline) and then take 6 digits as otp. Create a transaction and in the same transaction create the otp and idempotency row. If we fail in creation of idempotency row send 409 conflict only one service should go ahead. In the same trasnaction write a outbox table event for the newly generated otp. OTP sending flow: The CDC workers will pick up the events from the outbox table and then put the event in the kafka queue. They will guareente atleast once delivery The events will be picked up by the notification workers and then send the event based on the selected channel. OTP verification flow: The clients will send the otp/verify with the client entered otp and we will verify accross our db if the clientId matches, otp matches and the otp is not expired, also validated that the otp is not consumed before. Error handling. Idempotency will handle the once generation of OTP per key,client The outbox cdc + kafka will handle the atleast once delivery guareentees If the SMS service / email services goes down and we are not able to send the notifications with a exponential retry mechanism. if still not possible maybe if we have secondary provider we can switch else put this in our DLQ -> log it for audit but dont send because otp are time sensitive. If we are reaching a limit and the queue is increasing we can spin up more notification servers to tackel the load. we can use serverless compute and keep a pre warm pool. We can also create different queues for short, medium and long lived otps and treat them as differnt priority because its necessary to deliver the short lived otp first. The api gateway which does the rate-limit auth will also perfomr the fraud detection and block the ips from which fraud , DDos brute force attack is discovered.

Hire SignalLean Hire

This is a credible design with good senior-level instincts around idempotency, async delivery, and consistency, and it clearly addresses the stated functional requirements. The main reason it does not score higher is that the verification path—the correctness-critical part of an OTP system—has important gaps around ownership binding, atomic consume semantics, and explicit failure/degradation behavior.

⭐ Excellent

Consistency requirement is explicit and reflected in verification semantics

The design clearly calls out strong consistency for OTP verification and ties it to concrete checks during verify: matching client, expiry, and consumed state. For OTPs, choosing the wrong consistency model can allow duplicate use or acceptance of stale codes, so explicitly anchoring verification on the authoritative database is a strong NFR decision.

✅ Good

Delivery guarantee is defined with a concrete mechanism

The candidate does not just say 'reliable delivery' — they connect the at-least-once requirement to a transactional outbox plus CDC and Kafka. That shows the delivery guarantee is being treated as a non-functional requirement that drives the design rather than a vague aspiration.

✅ Good

Latency sensitivity is acknowledged in downstream prioritization

Separating short-, medium-, and long-lived OTPs into different queues is a thoughtful way to align delivery behavior with the stated propagation target. It shows awareness that not all OTPs have the same tolerance for queueing delay.

warning

Latency targets are stated but not defended against the 10M/day assumption

You give concrete targets like generation under 100ms and propagation under 5s, but what happens during peak traffic if 10M/day is bursty rather than uniform? Without connecting those SLOs to expected peak QPS, queue depth, worker throughput, and provider latency budgets, the numbers are floating in isolation and it's hard to tell whether they are actually achievable.

warning

Availability is named, but failure-mode expectations are not made explicit

Have you considered what happens to your availability target when the database, CDC pipeline, Kafka, or notification provider is degraded? You mention retries and DLQ, but there is no concrete availability objective or statement of which failures should still allow OTP generation versus verification. For a senior-level NFR discussion, I would expect the availability goal to drive clear degradation behavior.

warning

Strong consistency is chosen for verification, but the trade-off is not articulated

What happens when the primary database is slow or unavailable? With strong consistency on verification, the system may reject or delay valid OTP checks rather than risk stale reads. That may be the right choice, but the design should explicitly justify why correctness beats availability here and what user-visible behavior is acceptable under that trade-off.

info

At-least-once delivery needs an explicit user-facing interpretation

You could improve this by clarifying what at-least-once means for OTP notifications. With retries and multiple providers, users may receive duplicate OTP messages. That is usually acceptable if verification semantics are idempotent and only the latest valid OTP is accepted, but the NFR section should say that explicitly so the delivery guarantee is framed as an intentional trade-off rather than an accidental side effect.

✅ Good

Core OTP entity is tied to its lifecycle

The OTP entity is not treated as just a code string; the explanation clearly implies lifecycle state such as expiration and consumed/not-consumed. That is the right domain modeling for the happy path because generation, delivery, and verification all depend on those states.

✅ Good

Client is modeled as a first-class domain concept

Including Client as a core entity is a solid choice because OTP generation and verification are scoped by clientId, and the generation logic also depends on client-specific secrets. That establishes an important relationship between the caller and the OTPs it owns.

warning

User/request target is missing as a clear entity

Have you considered what happens when the same client requests OTPs for many different users or channels at the same time? The explanation mentions 'information about the user' and delivery channel, but the entity model only lists Client and OTP. Without a clear domain concept for the OTP target/request context, it is hard to reason about which OTP was sent to which phone/email and how verification maps back to the original request.

warning

Relationships between entities are only partially defined

Have you considered making the relationships explicit? From the explanation, a Client can create many OTPs, but it is not clear whether an OTP belongs to exactly one delivery target, one request, or one idempotency key. That ambiguity matters when you need to answer questions like 'can one user have multiple active OTPs?' or 'what exactly is being verified against?'

info

Supporting entities used in the happy path are implied but not modeled

You could improve this by explicitly calling out the domain concepts that already appear in your flow, such as Idempotency Record and Notification/Delivery Attempt. Since generation depends on idempotency and sending depends on channel-specific delivery, naming those entities would make the happy path easier to trace and the ownership of state clearer.

critical

Capacity planning is essentially absent

There are no basic scale numbers beyond '10M requests/day' and no translation into peak QPS, verification traffic, send traffic, queue throughput, storage growth, or provider call volume. What happens during peak hours or bursty login events? Without even back-of-the-envelope sizing, the chosen infrastructure could be underprovisioned by a large margin and fail under normal production spikes.

warning

No peak-load or traffic-shape reasoning

Have you considered what happens if OTP traffic is concentrated into short windows rather than evenly spread across the day? OTP systems are typically bursty around login peaks, promotions, or incidents. Using only daily request volume without estimating peak QPS leaves the API tier, database, Kafka, and notification workers unsized for the moments that matter most.

warning

Missing end-to-end load chain from requests to infrastructure

Have you considered walking the load through the full system: send requests -> DB writes for OTP/idempotency/outbox -> CDC throughput -> Kafka ingress/egress -> notification worker concurrency -> verify read/write load? The explanation mentions components and retries, but there is no logical chain showing that the architecture can sustain the assumed traffic.

warning

Storage and retention impact are not estimated

Have you considered how many OTP, idempotency, audit, outbox, and DLQ records accumulate over time and what retention policy is needed? Even if each OTP is short-lived, operational tables can grow quickly at 10M requests/day. Without estimating data volume and cleanup cadence, the primary database can degrade from table/index bloat.

warning

Retries and at-least-once delivery are not reflected in throughput sizing

What happens when an SMS or email provider is slow or failing and exponential retries kick in? At-least-once delivery plus retries can multiply queue traffic and worker demand well above the base request rate. Without accounting for retry amplification, Kafka partitions, consumer concurrency, and provider-side rate limits may be undersized.

info

Component choices are not justified by this scale

You could improve this by explaining why CDC + Kafka is warranted for 10M requests/day versus a simpler queue-based design, and what scale characteristic makes that trade-off worthwhile. The current explanation says Kafka helps delivery guarantees, but it does not tie that choice to expected throughput, burst handling, or operational needs at this load.

⭐ Excellent

Idempotent OTP creation is explicitly exposed

Using an Idempotency-Key on POST /otp/send is a strong API choice for a retry-prone flow like OTP generation. It gives clients a safe way to retry on network failures without accidentally creating multiple OTPs, and the explanation makes clear the service returns the previously created otpId/status when the same request is replayed.

✅ Good

Core flow is covered by two simple endpoints

The API surface is minimal but usable for the stated requirements: one endpoint to generate/send an OTP and one to validate it. For this problem, that keeps the client integration straightforward.

✅ Good

Verification result states are thought through

Returning distinct outcomes like pass, fail, expired, and invalid shows the candidate considered common OTP verification states rather than treating every non-success as the same case.

warning

Send response is underspecified for an async delivery flow

What does the client actually get back from POST /otp/send when notification delivery happens asynchronously through outbox/Kafka/workers? The route is shown as returning 'OTP', but the explanation says the OTP is sent via email/SMS and the API may return an existing otpId/status for idempotent retries. Without a clear response contract such as accepted/requested/sent/duplicate plus otpId and expiry metadata, clients will not know whether to prompt the user to enter a code, retry later, or surface a delivery failure.

warning

Error model and retry guidance are missing

Have you considered what the client sees on malformed input, rate limiting, duplicate idempotency conflicts, or temporary downstream failures? Right now there are almost no HTTP status codes or error shapes beyond one mention of 409. At this level, clients need predictable responses like 400 for invalid channel/type, 401/403 for auth failures if applicable, 429 for abuse/rate limits, and 5xx or 202-style async acceptance semantics for transient delivery issues, ideally with guidance on whether the client should retry with the same idempotency key.

warning

Verification API does not clearly identify the verification scope

What happens if a user has multiple active OTP requests or retries send and receives multiple codes over time? POST /otp/verify takes otpId and otp, which is workable, but the explanation also mentions validating clientId matches while that field is not present in the request. The API contract should make the verification scope explicit so the client knows exactly which OTP request is being verified and the server can safely reject cross-request or cross-user verification attempts.

info

Resource naming could be cleaner

You could improve this by making the REST shape more resource-oriented, for example POST /otps to create/send an OTP request and POST /otps/{otpId}/verify to verify a specific OTP. The current routes are understandable, but a cleaner resource model makes request identity and lifecycle easier to reason about.

info

Input contract needs tighter validation semantics

You could improve this by clarifying fields like 'user' and 'send_at'. As written, 'send_at' looks like a timestamp but is being used as a destination/channel selector ('email | phoneNo'). A clearer contract such as channel plus destination would reduce client confusion and make validation errors much more precise.

⭐ Excellent

Transactional outbox for send flow

Using the OTP row, idempotency row, and outbox event in the same database transaction is a strong end-to-end design choice. It closes the classic gap where an OTP is stored but the notification event is lost, and it fits the at-least-once delivery requirement well.

✅ Good

Idempotency handled on OTP creation path

Checking and persisting an idempotency key before creating a new OTP is a good way to make /otp/send safe under client retries and gateway timeouts. The candidate also thought through the race by relying on a DB constraint and conflict handling.

✅ Good

Async notification pipeline

Decoupling OTP generation from SMS/email delivery through CDC and Kafka is a good scalability choice. It keeps the synchronous API path short for the <100ms generation target and allows notification workers to scale independently.

critical

Verification flow is not fully connected to the request model

What happens on /otp/verify when the API only sends otpId and otp, but the explanation says verification checks clientId matches as well? In the current HLD, that binding is unclear, and if verification relies only on otpId + otp, a leaked otpId could let the wrong caller attempt verification. The design should make the ownership check explicit in the flow, either by carrying authenticated client identity through the gateway/service path or by storing and validating the request principal deterministically.

warning

Notification provider path is under-specified in the diagram

Have you considered what happens after Notification servers consume from Kafka? The HLD never shows a concrete connection from Notification servers to the SMS/email providers, and only Email service is annotation-connected while SMS is just drawn as a cluster. The end-to-end send path is understandable from the explanation, but the diagram leaves the most failure-prone hop implicit. Make the provider calls, retry path, and provider selection/failover explicit.

warning

Postgres is the likely bottleneck and possible SPOF at 10M/day

Have you considered what happens when all send requests, idempotency checks, OTP writes, outbox writes, and verify reads hit the same Postgres instance? Even if 10M/day is manageable, this database is the center of every critical path and also feeds CDC. If it slows down or fails, generation, verification, and delivery all degrade together. The design would be stronger if you called out HA for Postgres and how you isolate hot paths, for example primary/replica roles, partitioning by time/client, or keeping verify reads efficient with targeted indexes.

warning

Consumed-state update on verify is missing from the flow

What happens when two verify requests for the same OTP arrive at nearly the same time? The explanation says you check that the OTP is not consumed, but the HLD does not show the state transition that marks it consumed atomically. Without an explicit compare-and-update or transactional update, both requests can pass before either marks the row consumed. The design should show an atomic verification-and-consume step in the OTP service to preserve strong consistency.

info

Hot read path could benefit from targeted caching or in-memory optimization

You could improve this by reducing repeated database work on the send path, especially for idempotency lookups and possibly short-lived metadata used by notification workers. The current design is DB-centric for every operation. Even if OTP verification itself stays strongly consistent against the source of truth, selective caching of non-authoritative metadata or aggressive indexing would help keep latency stable under bursts.

info

DLQ recovery loop needs clearer ownership

You could improve this by clarifying what the Worker does after reading from the DLQ. Right now the only drawn action is Worker -> Postgres DB, which suggests failures are only recorded, not retried back into the delivery pipeline. If the intent is audit-only because OTPs are time-sensitive, say that explicitly in the HLD; if retries are intended, show how the message re-enters Kafka or the notification path safely.

Want this kind of feedback on your own design?

Draw your architecture for Design an OTP generation and Verification system and get an instant hire/no-hire signal from 6 specialized AI reviewers — free to start.