Code › tail-villain
Adding a Coding Round Without Splitting the Product
Adding standalone coding practice without breaking the existing session and dashboard flow
The coding round was buried inside the roadmap interview hub.
That placement made sense at first. A user builds a roadmap, chooses a topic, practices interviews, and sometimes wants coding practice as part of the same preparation loop. But as a product flow, it was too deep. If someone wanted to solve one coding problem, they had to enter through a roadmap context first. The dashboard also gave the feature too little visibility.
So I pulled coding practice out into its own surface.
The constraint was that it could not become a separate product. Tail Villain works when practice tools feel connected through the same dashboard and session language. Coding practice needed to be standalone without becoming an island.
The first boundary was the session model.
InterviewSession had assumed a roadmap. A standalone coding session does not necessarily have one, so roadmapId became nullable and sessionKind became the explicit divider between roadmap interviews and coding rounds. Coding problems moved into their own model, seeded from a Top 150 catalog.
Reusing the session model integrated the feature with the existing interview flow, but it also made query boundaries more important. If findActiveRoadmapSession does not filter by sessionKind, a coding session can be mistaken for a roadmap session. Any query that claims to find a roadmap session now has to say so in the database condition, not only in the function name.
On the frontend, coding practice got its own dashboard page. If an active session exists, the user can resume it. Starting a different problem asks for confirmation. The session list moved from client-side slicing to server-side cursor pagination. Completed and abandoned sessions have separate public states, because abandoning a coding round is not the same event as finishing one.
Those details matter because coding practice is naturally more interruptible. A user may start a problem, decide it is the wrong one, abandon it, and choose another. The product should represent that flow directly instead of flattening everything into completed.
The dashboard had to change with the feature.
If coding practice only lives behind a new route, users have to rediscover the product map. I reorganized the dashboard into a two-column bento layout: today’s roadmap focus and roadmap list on the left, practice tools and overall stats on the right. The old summary stat cards made the page feel fragmented, so the hierarchy moved into fewer surfaces.
Roadmaps became slim rows instead of full cards. The dashboard is not the place where a user reads every detail; it is where they decide what to continue. The new roadmap action moved to the panel footer with guide text and examples. When there are no roadmaps, the left panel disappears and the empty-state hero owns the page.
Coding practice sits inside Practice Tools. Writing and speaking practice remain Coming Soon, while coding is active. That placement was deliberate: the feature is no longer trapped inside a roadmap, but it still belongs to the same preparation platform.
Separating the session kind exposed smaller state bugs.
Back navigation has to depend on the session kind. A user leaving a coding round should return to the coding page, not a roadmap detail page. The AI prompt also needs an explicit language override; otherwise a Korean session can still receive an English problem statement. Opening turns are constrained to plain text so the chat does not suddenly inherit markdown-style formatting.
The catalog name was cleaned at the seed level rather than hidden only in the UI. If the source data keeps pulling an external brand name into the product, the UI will keep fighting it. The product should own the name it shows.
These are small fixes, but they are the cost of adding a new mode to a shared model. Reuse makes implementation faster, and sessionKind keeps reuse from turning into cross-contamination.
The next day showed that the product flow was not finished until it worked on mobile.
The bento dashboard looked organized on desktop, but xl breakpoints are not a mobile strategy. At 375px, the layout collapsed poorly. Roadmap detail still tried to hold a sidebar and content together. Interview headers squeezed navigation and status badges into one row.
I did a mobile pass across the dashboard, goals, backgrounds, roadmap detail, interview sessions, study sessions, chat room, and report dialog. The dashboard collapses to one column. Goal and background cards use full width. Roadmap detail stacks. Interview headers split into a navigation row and a status row. Chat bubbles stay within a readable width on narrow screens.
That pass also produced a design-system document. The warm surface palette had been copied around through code archaeology: page background, panel, card, input, hover state. Writing it down made later edits less dependent on memory.
Mobile also forced lower-level interaction fixes. iOS needs visible active states for taps. Text inputs need a 16px font size to avoid focus zoom. Fixed buttons should not lose viewport positioning because a parent created the wrong overflow context. Those are not decorative details. They decide whether the product feels stable in the hand.
On May 28, the remaining work shifted toward state integrity.
The report dialog showed a blank next review date for some roadmap-level sessions. The first instinct was to add a nextReviewAt column to InterviewSession, but that put the data in the wrong place. Review scheduling belongs to Topic for topic-scoped spaced repetition. Roadmap-level sessions intentionally do not have the same topic record.
The correct fix was to derive the value in the DTO layer where possible instead of adding a column. A database column looks permanent and clean until it starts duplicating ownership. If a value can be derived safely, derivation should be checked before migration.
Roadmap polling had another state problem. The page polled every few seconds and called setRoadmap even when nothing meaningful had changed, which could freeze mobile scrolling. I added a fingerprint gate: analysis status, topic generation status, topic count, roadmap updatedAt, and the newest topic updatedAt. The last item mattered because interview completion mutates leaf topic state such as HP bars and score badges.
Production throttling exposed a similar boundary. The HEAVY tier was intended for expensive LLM endpoints, but the global throttler applied it to undecorated endpoints too. A roadmap detail poll every three seconds burned through a 10-per-hour limit in half a minute. The fix made HEAVY opt-in through explicit metadata and added 429 backoff to the frontend poll loop.
Adding the coding round was not just adding an algorithm page.
A new practice mode changes session ownership, active-session lookup, return paths, pagination, dashboard hierarchy, mobile layout, polling behavior, and throttling assumptions. It needs to be discoverable as its own tool while still speaking the same product language as the rest of Tail Villain.
There is a difference between separating a feature and splitting a product.
Coding practice was too hidden when it lived inside the roadmap hub. But if it became a separate island, the platform would start fragmenting. The work was to hold that boundary: standalone entry, shared session model, explicit session kind, coherent dashboard placement, and mobile interactions that preserve the same flow.