Shibil Mohammed
Back to all work

Take-home concept · Fintech operations · 2025

A collections console where the fast path is the safe path.

One agent, a 1,247-loan queue, and bulk actions that carry compliance weight. Autodial, legal notice, settlement. A mis-scoped bulk action is not a UX error, it is borrower harm with the agent's name on it.

Engagement
Take-home, product designer, solo
Scope
Redesign of a collections queue and its bulk action model
Grounded in
Collections domain mechanics: DPD buckets, the promise-to-pay lifecycle, escalation stages
Evidence
No agent shadowing. No operator has used this. Unshipped.
The collections queue at 1440px: a filter rail with live facet counts, a shift summary strip, a compact table of delinquent loans with status tags and DPD, and a bulk bar carrying the selected count and the available actions
Three loans selected. The bulk bar exists because a scope exists, and the count sits in the same container as the buttons that will act on it.

At a glance

Prioritisation is the entire job, and the baseline tooling shows a list, not a queue. This console treats queue construction as the core act: facet counts show where the work is before a filter is applied, chips keep the queue definition auditable, and scope safety keeps throughput tools from becoming compliance incidents.

Page-scoped select-all, a bulk bar that carries the count, undo instead of confirmation. The safety budget is spent on scope, and the actions stay fast.

The failure sits in three layers

Building today's work is buried

The loans worth calling are a combination of criteria: DPD bucket by status by promise-to-pay date. When filters are scattered and do not stay visible, agents cannot quickly isolate a segment, and cannot see afterwards what segment they acted on.

The actions are heavy, and bulk makes them heavier

An autodial campaign, a legal notice, a settlement offer. These are not UI events, they are legal artefacts. A mis-scoped bulk action is borrower harm and a compliance incident, and agents could not tell how many loans were selected or which actions would apply.

Eight hours in one table

When every row looks the same, scanning has no rhythm. Fatigue is not a comfort problem in an ops console. It is where errors come from.

The cost compounds in both directions. Slow queue construction eats call time, and one wrong-scope notice costs more than a week of throughput gains.

The fast path and the safe path have to be the same path

Ops tools usually trade one for the other: add speed, then bolt confirmation dialogs on top. This design refuses the trade by relocating safety.

One distinction shaped everything. Bulk mistakes come in two kinds: action mistakes, wrong verb, and scope mistakes, right verb aimed at the wrong loans. Almost all real damage is scope. So the design invests its safety budget in scope construction, filters, counts, chips, selection, and keeps the actions themselves fast.

One rule kept it honest: safety lives in visibility, never in interruption. A mechanism that slows every action to guard against a rare mistake gets trained away within a week.

Four constraints, and where each one bites

ConstraintWhat it meantThe decision it forced
A wrong bulk action is a compliance incident Notices and settlements are legal artefacts, not reversible UI events. The deferred bulk bar, the always-visible selection count, and capping select-all at the visible page.
Agents are measured on throughput Any safety mechanism that adds a click per action will be defeated by muscle memory. Undo over confirmation, and ambient scope visibility instead of modal interruption.
The queue never empties 1,247 loans are worked in slices, across pagination, across the day. A persistent filter rail, facet counts, and state that survives pagination and reloads.
The table is an eight-hour environment Scanning cost compounds over a shift. 56px compact rows, zebra striping, weight hierarchy, right-aligned numerals, and a strict colour budget.

The user, at hour seven

Ravi, collections agent. He works a 1,247-loan portfolio. A shift is a couple of hundred contact attempts, and his pay depends on recoveries and promise-to-pay conversions. His risk runs the other way: one legal notice sent to the wrong segment is an escalation with his name on it.

His tension is structural. Every feature that makes him faster makes his mistakes bigger. Bulk selection, autodial, one-click notices: throughput and blast radius are the same dial.

The design target is hour seven, when attention is degraded and the queue is still full. If the interface prevents a scope error at hour seven, hour one takes care of itself.

One loop: define a scope, verify it, act, see the result

Filter rail. Fixed facets, status, DPD bucket, call attempts, last contact outcome, PTP date, each with a live count. The counts turn the rail into a portfolio summary: the agent sees where the work is before building the queue.

Chips. Active filters sit above the table as removable chips. The queue's definition is always visible, which in collections is an audit answer, not a nicety.

Selection to bulk bar. Selecting any row summons a bottom bar carrying the count, the relevant actions, and "Select all 50 in view." No selection, no bar. Scope exists before actions do.

Act, feedback, undo. Every step returns a visible response, and state survives pagination and reloads, so a queue is never rebuilt from memory.

The account detail drawer: contact numbers, outstanding and DPD, call, WhatsApp and SMS actions, a six-month payment history chart, an interaction timeline, and an outcome logging form with PTP amount and date
The detail drawer opens beside the queue rather than replacing it. Payment history, the interaction timeline with who did what and when, and outcome logging in one place. The note field is addressed to the next agent, not to a database.

Three mechanisms carry "no action without a visible scope"

The baseline puts actions in a toolbar, always live. It looks simpler, and it makes scope invisible: an agent can fire an action without ever seeing how many loans it will hit.

A queue is a filter combination, and the definition stays on screen

The facet counts show workload distribution before any filter is applied. The chips show exactly what the current queue means after. When a compliance question arrives, why was this borrower contacted, the answer was on screen at the moment of action: this status, this DPD bucket, this PTP state.

Scope is capped and counted

Select-all is page-scoped by design: "Select all 50 in view," never "select all 1,247." The global select-all is the single most dangerous affordance in bulk software when the verbs are legal notices. Capping scope at the visible page means the largest possible mistake is 50 loans, not the portfolio. The count travels with the bulk bar, so the number about to be affected is attached to the button that affects it.

Undo replaces confirmation

At two hundred actions a day a dialog becomes muscle memory within a week and then protects nothing, while taxing every legitimate action forever. Safety moved from interruption to visibility and reversal. Persistence does the quiet remaining work: filters, selections and sort survive pagination, because a queue rebuilt from memory at hour seven is where scope errors are born.

Specification board showing four states of the bulk bar: nothing selected with ghosted actions and no bar, two selected with the count and an indeterminate header checkbox, all fifty in view selected, and a selection spanning pages with off-page scope reported
The bulk bar as a specification, with the five rules underneath it. Off-page scope is reported, never silent, and changing the queue definition clears the selection, because a selection spanning a changed queue is exactly the invisible-scope failure this design exists to prevent.

The states an ops console is actually judged on

A queue tool spends a meaningful share of its life not showing a queue. Four of those states are designed rather than inherited.

The loading state: skeleton rows in the table while the filter rail and its counts are already rendered
Loading. The rail and its counts arrive first, so the agent can start reading the portfolio before the rows land.
The empty state: no accounts assigned, with an action to pull from the unassigned pool
Nothing assigned. Not an apology. The next action is the content.
The filtered-to-zero state: no accounts match these four filters, with the active filter chips still visible and a clear filters action
Filtered to zero. It names how many filters are responsible and leaves the chips on screen, so the agent can see which one to remove.
The error state: the queue could not load, with a retry action and a note that filters and position were not lost
Failure. "Your filters and position were not lost" is the sentence that matters. Losing a queue at hour seven is the real cost of an error, not the error.

Key product decisions

DecisionThe obvious buildWhy I rejected itAccepted cost
Page-scoped select-all Global select-all. It is faster for whole-segment operations. It converts one misread filter into a 1,247-loan incident. Processing a full segment takes multiple passes. A slower correct action beats a fast unrecoverable one.
The bulk bar appears with selection and carries the count A persistent action toolbar. Persistent actions divorce the verb from its scope. Here the actions surface only when a scope exists, with the count attached. Actions are less discoverable at rest, mitigated by the ghosted top bar that teaches the vocabulary without being fireable.
Undo over confirmation dialogs "Are you sure?" before every destructive action. Confirmation fatigue. Interruptions decay into reflexes. Visible scope and reversal do not. Undo demands real engineering, action queuing and grace windows, and some actions such as a sent notice have a short reversibility horizon that must be honest.
Counts on every facet, before filtering Plain checkboxes. Counts let the agent read the portfolio's shape, where the broken promises are piling up, before committing to a queue. Live counts are a real backend query load. The design assumes that investment and I have not costed it.
Compact by default, density as a control Comfortable spacing. At this row volume, comfort is throughput theatre. 56px is a working default that some agents will find tight, which is why the density control returns the choice to them.

The direction I did not take

An alternative collections queue with tabbed views, a filter rail, and a selection bar offering both select all on this page and select all matching loans
Note the selection bar: "Select all on this page" beside "Select all matching loans (42)." Offering both puts the portfolio-wide action one click from the page-wide one, at the moment the agent is least likely to read the difference.

Rhythm is the system

A 32px margin frame and an 8px spacing grid keep the layout stable while filters and selections change: nothing shifts that does not have to. Type carries hierarchy through weight, not size. Bold names, regular metadata. Numerals right-align for column scanning.

Colour is a budget spent almost nowhere: status tags, and a red DPD, the one column allowed to shout because it is the escalation driver. In an eight-hour tool, when everything signals, nothing does.

Table anatomy specification: 44px header, 56px compact row, zebra striping, hierarchy by weight, mono for identifiers, tabular right-aligned numerals, colour limited to status tags and DPD, and a two-line activity cell
The table as a spec rather than a picture. Every rule states what it buys: mono for identifiers so loan IDs are comparable at a glance, tabular numerals so money and DPD scan vertically as columns of digits.

What I would test, and what would settle it

Unshipped, so no outcomes. Four falsifiable bets, each with the metric that decides it.

The deferred bulk bar reduces scope errors

The central claim. If it fails, the whole safety model is decoration.

Metric, and kill criteriaMis-scoped actions per 1,000 bulk actions, and undo-within-10-seconds rate, against the persistent-toolbar baseline. No difference means the deferred bar costs discoverability and buys nothing.

Facet counts change how queues get built

The counts carry a real backend cost, so they have to change behaviour, not just inform.

MetricTime from shift start to first action, and the share of actions landing on priority segments such as broken promises and high DPD.

Rhythm improves scan performance

The density argument. It cannot be demonstrated on uniform rows.

MetricTime and error rate on a locate task, find the loans matching two criteria in a 50-row page, compact-with-rhythm against uniform rows.

Persistence cuts rework

Cheap to build, easy to measure, and the one I am most confident about.

MetricShare of sessions where an identical filter set is rebuilt after pagination or reload.

Limitations, and what comes next

I designed this without contact with a working collections agent, and collections floors are among the most observable environments in fintech. That is the first gap to close, and the cheapest.

The console has its idle and failure states, but the action flows are one surface deep. Selection through autodial campaign through outcome logging, and the states that go with them, partial bulk failure and an undo that arrives too late, are where an ops console is really judged and they are not built.

The facet counts assume backend query support I have not costed. And the demo dataset has to carry realistic variance, because a scanning-rhythm thesis cannot be demonstrated on uniform rows.

Next, in order: shadow or interview agents; rebuild the mock on a realistic loan distribution; extend one action flow end to end, including its failure states.

In bulk tools, the unit of safety is not the action. It is the scope.

People do not make action mistakes in bulk software. They make scope mistakes, the right verb aimed at the wrong rows. The instinct is to guard the verb with dialogs, warnings and friction. The work is making the scope impossible to misread, so the verb can stay fast.

A confirmation asks the user to be careful. A visible scope makes carefulness unnecessary.