Why mentions break at scale
At the start of developing a mention system, everything looks trivial: we find @ in the text, pull the user list, highlight matches. Problems begin when the audience grows from hundreds to tens of thousands, and the feed goes from a single screen to infinite scroll.
It is at this scale that three subsystems diverge, which in the prototype lived in a single function: mention parsing, user autocomplete, and mention highlighting.
The first failure is the desync between highlighting and links. The parser splits the string by word boundaries and Unicode whitespace, while the renderer highlights using a regular expression with different boundary logic.
As a result, a name with a hyphen or a dot is highlighted in full, but the link points to a truncated nick — the user sees one thing and navigates to another.
This is fixed by a single source of truth: the parser returns not a string, but a list of ranges "start-end + user identifier", and the renderer and link building work only off it.
The second failure is autocomplete lag. While a request goes to the server on every keystroke, the suggestion list arrives several characters behind, and the user selects the wrong person.
The solution is a local index of frequent contacts and debouncing with cancellation of stale requests, so that the response to "@an" does not overwrite the response to "@anna".
The third failure is duplicate mention notifications. As the audience grows, a single event passes through several handlers: post creation, editing, repost — and each sends its own notification.
An idempotent key "recipient + object + event type" is needed, by which repeated delivery is suppressed.
Let's break down how this is closed at the architecture level.
What a well-built mention system delivers
Autocomplete speed is the first thing a user notices. When the candidate list appears after the first character is typed rather than after a pause, entering a mention takes less than a second instead of several.
We keep the index of users and entities in the client's memory and update it incrementally, so the response does not depend on the size of the directory — whether it holds hundreds or hundreds of thousands of records.
Recognition accuracy is solved at the text-parsing level, not with regular expressions.
The parser accounts for word boundaries, escaping, nested mentions, and keyboard-layout differences, so "@иван" and "@Иван" lead to the same profile, while an email address or a handle inside a link does not turn into a false mention.
The share of correctly recognized entities on a labeled set of messages approaches the upper bound — the remainder comes down to typos, which the "did you mean" suggestion covers.
The load on support drops as a consequence: complaints like "mentioned the wrong person" and "the notification didn't arrive" leave the stream of requests, because choosing from a suggestion eliminates manual name entry.
Engagement in discussions grows through the same mechanism — the person who was tagged receives a notification with context and goes to the thread instead of losing the message in the feed.
We build the system on Kotlin and native rendering of the suggestion list, so that behavior matches on both platforms.
The metrics — suggestion response time, the share of successful notification click-throughs, the number of mention-related requests — we surface on a shared dashboard, so that the effect is visible in dynamics rather than in words.
Approaches to parsing and autocomplete: a comparison
There are three working schemes, and the choice between them is determined not by taste, but by where the application's backend is and how live the user list is.
If the directory is small and rarely changes, parsing right in the application gives an instant reaction to input; if there are tens of thousands of users and they actively rename themselves, the only source of truth is the server. The hybrid covers both cases, but requires careful synchronization.
| Option | When it fits | Limitations |
|---|---|---|
| Client-side parsing | Directory of up to several thousand records, rarely changes, offline operation matters | The list goes stale between app updates; as the database grows, memory and search time grow |
| Server-side parsing | Large live database, frequent renames, uniform rules for all platforms | Every keystroke is a network request; on a poor connection suggestions lag, a fallback mode is needed |
| Hybrid scheme | The directory is large, but the active part is narrow (correspondence is with a dozen people) | More complex merge logic: you need to decide whose version of a record is fresher and clean up stale ones |
How to choose for your profile
The key indicator is how many unique names actually end up in the suggestion during a session.
If it's dozens, a local suggestion cache on top of server-side parsing removes almost all the load: the application keeps recent interlocutors and frequent contacts locally, and pulls the rest with a request.
Such a cache is updated by time-to-live and when a dialog is opened, so a rename is picked up without a restart.
What breaks at the junction
The main mistake is diverging on parsing rules: the client counts one construct as a mention, the server another, and broken links appear in notifications.
The second source of failures is a deleted user: the record remains in the cache, and when sending it turns into an empty link. That is why parsing rules are kept in one place, and recently deleted records are marked rather than cut out immediately.
How we implement a mention system
We start with an audit: we look at how the app currently behaves when "@" is entered — where the suggestion triggers, where the text goes through as-is, how the identifiers of mentioned entities are stored.
At this stage we record edge cases: a mention at the beginning of a line, after a punctuation mark, inside a link, pasting from the clipboard, input from a hardware keyboard. Each case gets an expected behavior — this is the basis for acceptance, not guesswork by eye.
Next we design the parsing scheme and build it step by step.
- Audit of current behavior. We run input scenarios on the live app, collect discrepancies between platforms, and record which entities can be mentioned at all.
- Text parsing scheme. We define the mention grammar: allowed characters, token boundaries, escaping rules. Parsing runs in a single pass over the string, so entering long text does not slow things down.
- Autocomplete setup. The suggestion is requested with a delay after the last keystroke and cancels the previous request if the user keeps typing. We rank the list by match and frequency — the relevant one ends up first.
- Linking to data. We store a mention as an entity identifier, not as text: when a user or channel is renamed, the link stays working.
- Scenario testing. We run the parser on a prepared set of strings — with emoji, mixed alphabets, interrupted input, deletion of a character inside a mention. We separately check behavior when the network is unavailable.
- Load testing. We look at how the suggestion behaves with a large user base and on a low-end device: response time and memory consumption.
- Documentation handover. We hand over the parsing specification, the description of the mention storage format, and the acceptance checklist so the client's team can develop the system on its own.
After handover we stay on support: changes to parsing or ranking rules go through the same set of checks as the initial implementation.
Artifacts: configurations, schemas, access policies
After project handover, you are left not with a "folder of source files" but with a set that lets the mention system live and evolve without us. Each artifact addresses its own class of tasks: text parsing, suggestions, permissions, notifications, operations.
- Mention parsing schema — a formalized description of how the parser finds entities in text: token boundaries, the set of allowed characters, behavior with Cyrillic and Latin script, handling of nested and repeated mentions. The schema is tied to test cases, so any change to the rules is immediately checked against regressions.
- Suggestion configuration — autocomplete parameters: trigger threshold, result size, sorting priorities, candidate sources (local cache, server-side search, recent contacts). Can be changed without rebuilding the application.
- Access policies — a "who can mention whom" matrix: private channels, closed groups, external participants, blocked users. The rules are enforced both on the client and on the server, so bypassing the interface does not grant access to other people's data.
- Notification policy — when a mention turns into a push, how repeated ones are grouped, what happens when offline, and how the delivery log is maintained. Scenarios where a notification is intentionally not sent are described separately.
- Mention documentation — the storage format specification, server method contracts, a description of edge cases, and a support guide: how to reproduce an issue and what to look at in the logs.
- Migration and rollback schema — the rollout order, backward compatibility with older client versions, and the degradation shutdown plan.
- Automated test suite — coverage of parsing, suggestions, and access permissions, including negative scenarios.
The set is handed over along with the repository and a short session for your team: we go over what is where and how to make changes. Describe your task — we will get back to you with an estimate and a plan for your application.
Case study: sped up autocomplete and removed duplicate notifications
The client is a collaboration service with a discussion feed. Mentions worked, but suggestions appeared with a delay, and with fast typing and edits, users received two or three notifications about the same thing.
Let's break down the mentions case study: what was measured, what was changed, and what before/after metrics were obtained.
What it was before
Autocomplete requested the full list of participants on every keypress and filtered it on the client. In active discussions, this caused a noticeable lag: the suggestion was rendered when the user had already finished typing the name.
Notifications were sent for every input event — when editing text or mentioning the same person again in the same message, several pushes were sent.
What was changed
- Moved filtering to the server: the request is sent with a prefix after the
@symbol, and the response contains only relevant matches, limited in number. - Added a cache of frequent discussion participants so that typical suggestions are returned instantly.
- Introduced notification deduplication by the "author — recipient — message" pair: a repeated mention in the same message does not generate a new push.
- Separated input and send states — the notification is sent only after the message is actually saved.
Before/after metrics
| Metric | Before | After |
|---|---|---|
| Suggestion speed | noticeable delay while typing | suggestion appears almost immediately |
| Duplicate notifications | 2-3 pushes per mention | one push per message |
| Erroneous notifications | regular complaints | reduced to zero |
The work was carried out by our engineers on React Native — this made it possible to reuse the parsing logic across platforms and avoid duplicating it separately for each OS.
After the release, the number of support requests on the topic of notifications decreased, and the share of messages with mentions increased: suggestions stopped interfering with typing.
How does client-side parsing differ from server-side parsing?
The difference shows up the moment a user types the @ character and starts entering a name. Client-side parsing means the application itself finds the boundaries of the mention in the text and filters the list of participants using an already loaded directory.
Server-side parsing is when the string is sent to the backend, parsed there, matched against the user database, and a ready result is returned. Both approaches solve the same problem, but they distribute load and latency differently.
Client-side parsing wins on responsiveness: the suggestion appears in a fraction of a second because it doesn't wait for the network.
But the participant directory has to be kept in the device's memory and synchronized — with large teams, that's noticeable weight and a risk of stale data.
Latency here is minimal, but the load on the device grows: parsing the text, filtering, and rendering the list fall on the phone's processor, which is noticeable on low-end models with long messages.
Server-side parsing takes the load off the device and always works with an up-to-date directory — there's no need to cache or synchronize anything.
The price for this is network latency on every request and a constant stream of calls to the backend: during active typing, that's dozens of requests per second per user.
Without proper request batching and server-side caching, the load on the infrastructure grows linearly with the audience.
In practice, a hybrid is more often chosen: parsing and highlighting of mentions happen on the client, while search and permission checks happen on the server. This way, latency stays within perceptible limits, the device isn't overloaded, and the directory is always up to date.
The cost of maintenance is lower than with a pure server-side approach: less traffic, simpler scaling, predictable load on the backend. The choice is dictated by the scenario — audience size, frequency of mentions, and requirements for data freshness.
Common technical concerns
False positives almost always arise at the junction of the parser and the nickname validation rules, not in the matching itself.
We parse the string character by character, taking into account word boundaries and the set of allowed characters, so an email address, a link, or a code fragment does not turn into a mention.
We maintain a separate list of exceptions — service words, emoji sequences, repeated occurrences within a single message: each rule is covered by a test, and the share of erroneous matches stays at the level of statistical error.
Privacy is handled at the level of the access model, not through interface cosmetics. Autocomplete returns only those users whom the message author is entitled to see: private teams, hidden profiles, and blocked accounts do not appear in the results at all.
The server re-checks the right to mention at the moment of sending — if access was revoked between typing and publishing, the mention is not created, and the author receives a clear error instead of a silent loss.
What will happen to older versions of the app that are still in use?
We design mention markup as an extensible format with versioning: an old client is unaware of new entity types but continues to correctly display the ones it already knows. Unknown constructs degrade to plain text rather than breaking the message. This allows the app to be updated gradually, without forcing all users to migrate on the same day.
Will autocomplete create peak load on the server?
The search runs with input debouncing and cancellation of the previous request, results are cached by prefix, and frequent suggestions are served from memory.
As the number of users grows, read replicas are added — the results remain responsive, and the primary database receives no extra queries.
We close each of these concerns at the design stage, not after launch: the parsing rules, the access model, and the markup format are fixed in the documentation and covered by automated tests.
Let's discuss your mentions task
To make the estimate realistic rather than an "industry average," we need to understand your data model and your mention use cases.
We break the task down point by point: where the entities live, how search over them is structured, what constraints exist on access rights and directory size. Then we come back with an implementation plan, stage-by-stage timelines, and scope of work — no vague generalities.
Here's what we clarify on the first call:
- Where the entity source is — users, teams, documents, projects: one table or several disparate directories, and how they sync with the backend.
- How a match is found — by string prefix, by substring, by typos; whether Latin characters are needed alongside Cyrillic and how compound names are handled.
- Volume and frequency — how many entities are in the directory, how many requests per minute, whether server-side results are needed or a local cache on the device is enough.
- Access rights — does the user see only their own colleagues or the entire directory; how a mention behaves if the recipient has no rights to the object.
- Storage format — how a mention is embedded in the message text: an identifier alongside the display name, so that renaming doesn't break history.
- Behavior on deletion — what to show if the mentioned user or object no longer exists.
- Platforms and integrations — whether recipient notifications are needed, tap-through to a profile or card, and offline operation.
After that, we lock down the implementation plan: data model markup, text parser, autocomplete service, suggestion UI, edge-case handling, and tests on real scenarios. We break the timelines down by stage so you can see what you get and when.
Describe the task — where the entities are currently stored, which mention scenarios are critical, and which platforms the app runs on. We'll come back with a work estimate, an implementation plan, and timelines for each stage.







