Fluent Swap: как я вышел за пределы CRUD и начал разрабатывать matchmaking на Go
- вторник, 1 сентября 2026 г. в 00:00:11
За время работы с backend‑разработкой я реализовал достаточно REST API и TODO‑подобных приложений. На них удобно отрабатывать HTTP, взаимодействие с базой данных, разделение приложения на слои и обработку ошибок. Однако в какой‑то момент задачи начинают повторяться и сводиться к одному сценарию:
принять данные → сохранить в базе → вернуть ответ;
принять запрос → получить данные из базы → вернуть ответ.
Такой опыт создаёт необходимую основу, но почти не затрагивает долгоживущее состояние, конкурентный доступ, двустороннее взаимодействие с клиентом и проблемы, которые появляются при горизонтальном масштабировании.
Мне хотелось выбрать проект, в котором можно последовательно пройти эти этапы на практике: начать с простого решения, определить его ограничения и только затем добавлять Redis, Kafka и другую инфраструктуру. Так появился Fluent Swap — сервис для быстрого поиска собеседника для языковой практики.

Я в очередной раз решил серьёзнее заняться английским и понял, что мне особенно не хватает разговорной практики.
Сейчас для этого удобно использовать ChatGPT. Но общение с ИИ не полностью заменяет живой разговор, в котором нужно воспринимать речь другого человека, быстро формулировать ответ и справляться с неизбежными ошибками.
Идея Fluent Swap заключается в простом обмене: я помогаю носителю английского практиковать русский, а он помогает мне практиковать английский.
Подобные приложения уже существуют, поэтому задача проекта не в том, чтобы представить идею как уникальную. Для меня интереснее самостоятельно спроектировать и реализовать backend, в котором есть matchmaking, WebSocket‑соединения, конкурентный доступ к состоянию и дальнейший переход к нескольким экземплярам приложения.
Первая версия продукта должна быть максимально простой для пользователя.
Пользователь открывает приложение, выбирает родной и изучаемый языки, запускает поиск и после успешного совпадения переходит к разговору.
Например, пользователь с родным русским и изучаемым английским должен найти собеседника, у которого родной язык — английский, а изучаемый — русский.
На первом этапе не нужны регистрация, постоянные аккаунты и подробные анкеты. Пользователь должен зайти, нажать несколько кнопок — и начать поиск.
Минимализм здесь не только способ сократить объём первой версии. Это часть пользовательского сценария: между желанием попрактиковать язык и началом разговора должно быть как можно меньше действий.

Во многих backend‑проектах SQL‑база появляется почти автоматически. В Fluent Swap первая реализация хранит очередь ожидающих пользователей и активные комнаты в памяти процесса.
Изначально такой путь предложил мой наставник. Он позволил отделить требования продукта от привычного набора технологий и сначала ответить на более важный вопрос: какие данные действительно необходимо сохранять?
В минимальном сценарии пользователь приходит, находит собеседника, общается во временной комнате и завершает сессию. История сообщений пока не сохраняется. После завершения разговора серверу не требуется хранить данные этой сессии постоянно.
Поэтому на текущем этапе PostgreSQL не решает необходимую продуктовую задачу. Очереди в памяти достаточно, чтобы проверить основной сценарий и реализовать алгоритм matchmaking без дополнительной инфраструктуры.
Этот выбор также дал возможность отдельно поработать с конкурентным доступом. Очередь и связанные с ней индексы используются несколькими goroutine, поэтому операции над общим состоянием должны быть синхронизированы.
Разумеется, у решения есть ограничение: при перезапуске процесса всё временное состояние будет потеряно. Кроме того, память одного процесса невозможно напрямую разделить между несколькими экземплярами backend. Для текущей версии это осознанный компромисс, а не незамеченный недостаток архитектуры.
Даже минимальный matchmaking быстро приводит к вопросам, которые редко возникают в простом REST API:
Что произойдёт, если несколько пользователей одновременно начнут поиск?
Как не подобрать одного человека сразу двум собеседникам?
Как обработать повторный запрос от уже ожидающего пользователя?
Что делать, если клиент отключился во время поиска или активного разговора?
Кто отвечает за жизненный цикл WebSocket‑соединения?
Что произойдёт, если найденный пользователь перестал читать сообщения?
Как гарантировать согласованность комнаты и индексов её участников?
Здесь уже недостаточно выполнить запрос к базе данных и вернуть результат. Необходимо определять границы атомарных операций, учитывать отмену через context.Context, защищать разделяемое состояние и предотвращать утечки goroutine.
В первой реализации операция поиска партнёра и добавления пользователя в очередь защищена с помощью sync.Mutex. В пределах одного процесса это позволяет сделать изменение состояния атомарным и не допустить повторного использования одного участника в двух совпадениях.
Корректность проверяется не только последовательными unit‑тестами. В проекте есть конкурентные сценарии, а весь набор тестов запускается с race detector. Это не доказывает отсутствие всех возможных ошибок синхронизации, но помогает обнаруживать фактические гонки данных в выполненных тестовых сценариях.

Сейчас Fluent Swap реализован как модульный монолит на Go с трёхслойным разделением ответственности внутри функциональных областей.
Domain‑слой определяет основные сущности и инварианты: языковую пару, идентификаторы клиентов и комнат, состав участников и ограничения сообщений.
Application‑слой координирует сценарии поиска партнёра, создания комнаты и передачи сообщения, но не зависит от конкретного WebSocket‑фреймворка или способа хранения данных.
Infrastructure‑ и transport‑компоненты отвечают за хранение состояния в памяти, WebSocket‑протокол, управление соединениями и сборку зависимостей приложения.
Такое разделение позволяет заменить in‑memory реализацию на Redis, не перенося инфраструктурную логику внутрь matchmaking‑сервиса.
На текущем этапе уже реализованы matchmaking с FIFO‑очередью в памяти, WebSocket‑протокол, создание временной комнаты, двусторонняя передача сообщений и очистка состояния после отключения участника.
Критичные сценарии покрыты unit‑ и интеграционными тестами. В CI выполняются проверка форматирования, go vet и тесты с race detector.
При этом проект пока нельзя называть production‑ready. Состояние находится внутри одного процесса, reconnect ещё не реализован, а отказ процесса приводит к потере активных очередей и комнат. Эти ограничения явно зафиксированы и определяют следующие этапы разработки.
Следующий этап проекта — перенос matchmaking‑состояния в Redis.
Причина появляется при запуске нескольких экземпляров backend. Представим, что клиент A подключился к backend-1, а подходящий ему клиент B — к backend-2.
client A → backend-1
client B → backend-2
Если каждый процесс использует собственную очередь в памяти, эти клиенты не увидят друг друга. Ещё опаснее конкурентный сценарий, в котором два backend‑инстанса одновременно пытаются подобрать одного ожидающего пользователя разным партнёрам.
Обычный sync.Mutex такую проблему не решает: он синхронизирует goroutine только внутри одного процесса. Нескольким экземплярам приложения понадобятся общее состояние и атомарная операция, которая либо создаёт ровно одно совпадение, либо ставит пользователя в очередь.
Именно для решения этой проблемы в архитектуре появляется Redis. Перед написанием адаптера необходимо определить модель ключей, правила FIFO, идемпотентность повторных запросов, очистку временных данных и точную границу атомарности операции matchmaking.

Fluent Swap задуман не только как минимальный продукт, но и как основа для последовательной практики с инструментами современного Go backend. В план проекта уже входят Redis, Kafka, PostgreSQL, Docker, Kubernetes и observability. Я не собираюсь ждать, пока у приложения естественным образом появятся сотни тысяч пользователей и инфраструктура станет неизбежной.
Вместо этого каждый следующий этап будет создавать контролируемые условия, в которых можно исследовать конкретный класс задач. Для Redis таким условием станет запуск нескольких backend‑инстансов с общей очередью matchmaking. Для Kafka — появление доменных событий и нескольких независимых обработчиков. Для PostgreSQL — добавление данных с жизненным циклом дольше одной временной сессии.
Kubernetes также является запланированной частью проекта, а не отдалённой реакцией на реальную нагрузку. После подготовки приложения к горизонтальному запуску я разверну его в кластере и на практике разберу Deployments, Services, probes, rolling updates, resource limits и HPA. Нагрузочные тесты и моделирование отказов позволят проверить поведение системы без ожидания настоящего production‑трафика.
Таким образом, технологии будут появляться последовательно, но не случайно и не только под давлением реальных пользователей. Я намеренно буду формулировать реалистичное требование или failure scenario, реализовывать решение и проверять его ограничения. Это позволит изучить не только настройку инструмента, но и инженерную задачу, для которой он предназначен.
Fluent Swap начался как способ выйти за пределы знакомого CRUD, но уже в минимальной версии потребовал решений, связанных с конкурентным доступом, атомарностью, жизненным циклом WebSocket‑соединений и очисткой временного состояния.
Сейчас приложение умеет находить пару, создавать временную комнату и передавать сообщения между двумя клиентами. Следующая задача — спроектировать Redis‑модель и обеспечить корректный matchmaking при работе нескольких backend‑инстансов.
В следующих материалах я планирую разбирать проект через конкретные инженерные эксперименты: формулировать новый сценарий, показывать ограничения текущей реализации, сравнивать варианты решения и проверять необходимые гарантии тестами.
Архитектура проекта развивается от простого к сложному по заранее намеченному roadmap. Память одного процесса и локальный mutex стали отправной точкой, а Redis, Kafka и Kubernetes позволят последовательно перейти к задачам распределённого состояния, событийного взаимодействия и оркестрации нескольких экземпляров приложения.