Сервис для управления сегментами и их доступностью у пользователей Avito.
- Описание задания
- Запуск сервиса
- Спецификация API
- Детали реализации
- Вопросы, возникшие в ходе выполнения задания
- Прогресс выполнения поставленных задач
- Дальнейшнее развитие
В Авито часто проводятся различные эксперименты — тесты новых продуктов, тесты интерфейса, скидочные и многие другие. На архитектурном комитете приняли решение централизовать работу с проводимыми экспериментами и вынести этот функционал в отдельный сервис.
Требуется реализовать сервис, хранящий пользователя и сегменты, в которых он состоит (создание, изменение, удаление сегментов, а также добавление и удаление пользователей в сегмент).
Для начала необходимо склонировать репозиторий:
git clone https://github.com/TinyMarcus/avito-tech-task.git
Далее нужно перейти в директорию:
cd avito-tech-task
Запускается разработанный сервис в docker-контейнере с использованием docker-compose
. Во время запуска также поднимается база данных PostgreSQL
в отдельном контейнере, в которой создается база данных dynamic-user-segmentation
с таблицами segments
, users
, history
, users_segments
.
Для запуска следует воспользоваться командой docker-compose
. Приложение будет доступно на порту 8080:
docker-compose up -d
При этом при желании завершить работу сервиса и поднятой базы данных нужно вести следующую команду:
docker-compose down
Для упрощения запуска также можно воспользоваться утилитой make
, которая произведет сборку и запуск приложения:
make build
make run
Механизм миграции на данном этапе не реализован, создание базы данных и таблиц происходит запуском SQL-скрипта при инициализации контейнера.
- Разработанное API реализовано в соответствии с дизайном RESTful API.
- Тело запроса и ответа передается в формате JSON.
- При возникновении ошибки во время выполнения запроса в качестве результата будут возвращены HTTP-статус код в заголовке и описание ошибки в теле в формате
{"error": "message}
. - Реализована поддержка Swagger и Swagger UI для удобной работы с API во время разработки — для доступа к нему необходимо перейти по URL:
http://localhost:8080/swagger/index.html
.
Ниже приведена полная спецификация разработанного API с примерами запросов.
Добавление сегмента в БД.
- Тело запроса:
slug
— название сегмента;description
— описание сегмента.
- Тело ответа (код 201):
slug
— название сегмента.
Пример запроса:
Запрос:
curl -X POST localhost:8080/api/v1/segments \
-H "Content-Type: application/json" \
-d '{
"slug": "AVITO_VOICE_MESSAGES",
"description": "Голосовые сообщения в чатах"
}'
Ответ:
{
"slug": "AVITO_VOICE_MESSAGES"
}
Получение сегмента по названию.
- Параметры строки запроса:
slug
— название сегмента.
- Тело ответа (код 200):
- искомый сегмент.
Пример запроса:
Запрос:
curl -X GET localhost:8080/api/v1/segments/AVITO_VOICE_MESSAGES
Ответ:
{
"id": 1,
"slug": "AVITO_VOICE_MESSAGES",
"description": "Голосовые сообщения в чатах"
}
Получение всех сегментов.
- Тело ответа (код 200):
- список всех сегментов.
Пример запроса:
Запрос:
curl -X GET localhost:8080/api/v1/segments
Ответ:
[
{
"id": 1,
"slug": "AVITO_VOICE_MESSAGES",
"description": "Голосовые сообщения в чатах"
},
{
"id": 2,
"slug": "AVITO_PERFORMANCE_VAS",
"description": "Новые услуги продвижения"
},
{
"id": 3,
"slug": "AVITO_DISCOUNT_30",
"description": "Скидка 30% на услуги продвижения"
},
{
"id": 4,
"slug": "AVITO_DISCOUNT_50",
"description": "Скидка 50% на услуги продвижения"
},
]
Изменение сегмента в БД.
- Параметры строки запроса:
slug
— исходное название сегмента.
- Тело запроса:
slug
— новое название сегмента;description
— новое описание сегмента.
- Тело ответа (код 200):
id
— идентификатор сегмента;slug
— новое название сегмента;description
— новое описание сегмента.
Пример запроса:
Запрос:
curl -X PUT localhost:8080/api/v1/segments/AVITO_VOICE_MESSAGES \
-H "Content-Type: application/json" \
-d '{
"slug": "AVITO_VOICE_MESSAGES",
"description": "Новое демонстрационное описание"
}'
Ответ:
{
"id": 1,
"slug": "AVITO_VOICE_MESSAGES",
"description": "Новое демонстрационное описание",
}
Удаление сегмента из БД.
- Параметры строки запроса:
slug
— название сегмента.
- Параметры ответа:
- HTTP-статус код 204.
Пример запроса:
Запрос:
curl -X DELETE localhost:8080/api/v1/segments/AVITO_VOICE_MESSAGES
Добавление пользователя в БД.
- Тело запроса:
name
— имя пользователя.
- Тело ответа (код 201):
id
— идентификатор пользователя.
Пример запроса:
Запрос:
curl -X POST localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{
"name": "Ivan Ivanov"
}'
Ответ:
{
"id": 1
}
Получение пользовател по идентификатору.
- Параметры строки запроса:
userId
— идентификатор пользователя.
- Тело ответа (код 200):
- искомый пользователь.
Пример запроса:
Запрос:
curl -X GET localhost:8080/api/v1/users/1
Ответ:
{
"id": 1,
"name": "Ivan Ivanov"
}
Получение всех пользователей.
- Тело ответа (код 200):
- список всех пользователей.
Пример запроса:
Запрос:
curl -X GET localhost:8080/api/v1/users
Ответ:
[
{
"id": 1,
"name": "Ivan Ivanov"
},
{
"id": 2,
"name": "Petr Petrov"
}
]
Добавление пользователей в сегмент и удаление из них.
- Параметры строки запроса:
userId
— идентификатор пользователя.
- Тело запроса:
add_to_user
— сегменты, в которые будет добавляться пользователь;take_from_user
— сегменты, из которых будет убираться пользователь.
- Параметры ответа:
- HTTP-статус код 200.
Пример запроса:
Запрос:
curl -X POST localhost:8080/api/v1/users/1/changeSegmentsOfUser \
-H "Content-Type: application/json" \
-d '{
"add_to_user": [
{
"slug": "AVITO_VOICE_MESSAGES",
"deadline_date": "2023-09-30 12:00:00+03"
},
{
"slug": "AVITO_DISCOUNT_50"
}
],
"take_from_user": [
"AVITO_DISCOUNT_30"
]
}'
Получение активных сегментов пользователя.
- Параметры строки запроса:
userId
— идентификатор пользователя.
- Параметры ответа:
- HTTP-статус код 200.
- Тело ответа:
- идентификатор пользователя и список сегментов.
Пример запроса:
Запрос:
curl -X GET localhost:8080/api/v1/users/1/active
Ответ:
{
"userId": 1,
"segments": [
{
"slug": "AVITO_VOICE_MESSAGES",
"deadline_date": "2023-09-31 12:00:00+03"
},
{
"slug": "AVITO_DISCOUNT_50"
}
]
}
Также важно отметить, что все приведенные выше запросы для взаимодействия с пользователями и сегментами можно тестировать с использованием Swagger UI:
Ниже приведена структура проекта разработанного сервиса:
.
├── internal
│ ├── config // конфигурация приложения
│ ├── db // подключение к БД
│ ├── handlers // обработчики входящих запросов
│ │ ├── dto // DTO-модели
│ │ └── middlewares // миддлвейры (в частности для логирования запросов)
│ ├── logger // логгер и его конфигурация
│ ├── models // основные структуры для работы с сущностями БД
│ └── repositories // репозитории с методами для взаимодействия с БД
├── cmd/dynamic-user-segmentation-service // точка входа в приложение
└── api // документация Swagger
При разработке сервиса автор придерживался:
- следование GitFlow (была создана ветка
develop
, от которой создавались остальные ветки по мере продвижения реализации сервиса); - следованию дизайну RESTful API;
- подхода разделения севриса на разные слои (чистая архитектура), а также внедрения зависимостей;
- работы с СУБД PostgreSQL с использованием библиотеки
sqlx
и написанием сырых SQL-запросов; - использование docker и docker-compose для поднятия и развертывания dev-среды (с накатыванием схемы БД при запуске).
Также было начато, но недоделано:
- юнит-тестирование с помощью моков (использовались библиотеки
testify
иgomock
) — были созданы мок для репозитория, но из-за нехватки времени тесты не были написаны; - поддержка запроса истории — был реализован репозиторий с методами записи добавления и удаления пользователей из сегментов, но не написан метод и "ручка" для получения истории.
Ниже приведена разработанная схема базы данных для сервиса.
В базе данных всего 4 таблицы: таблица с пользователями, таблица с сегментами, таблица, связывающая пользователей и сегментов (связь многие-ко-многим), а также таблица с историей добавления/удаления сегментов у пользователя (связь многие-ко-многим).
Таблица с историей была создана для выполнения дополнительного задания №1 с запросом истории пользователей, но задание не было завершено — была реализована запись истории, но не ее получение.
Для поддержания чистой архитектуры и упрощения разработки сервиса в будущем архитектура сервиса была разделена на несколько слоев — на слой работы с БД и на слой обработки входящих запросов.
Для взаимодействия с БД был реализован паттерн "Репозиторий" и написаны репозитории для работы с таблицами пользователей, сегментов и истории (userRepository
, segmentRepository
и historyRepository
соответственно). Репозитории были реализованы для СУБД PostgreSQL — при необходимости работы с другими СУБД использованный подход позволяет с легкостью заменить реализацию репозиториев на новую необходимую.
Для обработки входящих запросов были реализованы хендлеры (или контроллеры), которые, принимая входные данные, вызывали в своих методах методы репозиториев, передавая туда всю необходимую информацию для записи в БД.
- Возник вопрос с хранением пользователей в БД данного сервиса в отдельной таблице — при эксплуатации в реальности сервису нет необходимости хранить информацию о пользователях отдельно, так как сервис должен работать только с привязкой пользователей к сегментам. Но для полноты представления "пайплайна" и хранения всей необходимой информации в рамках тестового задания (возможности запрашивать пользователей у меня не было, так как для этого нужно либо создавать отдельный сервис, либо получать информацию о пользователе из запроса (но тогда не было бы возможности делать проверку на существование пользователя, которую хотелось добавить)) я принял решение хранить информацию о пользователях отдельно в таблице
Users
. Именно по этой логике есть "ручка" для создания пользователя, но не его удаления или изменения. - Изначально у меня возникло желание для отображения идентификатора пользователя и сегмента использовать тип данных
uuid
вместоint
, так как на большом проде из-за существования в системе большого числа пользователей, а также для соблюдения безопасности, используется этот тип данных, но из-за того, что в условии идентификаторы пользователей были целочисленные, я решил использовать все же его :) - У меня было желание реализовать поддержку миграций, но я остановился на использовании SQL-скрипта для создания базы данных и необходимых таблиц. Но при этом, как я писал выше, схема создается автоматически при запуске через docker (в случае если она не существует).
- При выполнении дополнительного задания №2 возник вопрос, какие принадлежности сегмента пользователю считать "активными". Активными являются привязки, которые либо не содержат дату "протухания", так как они активны всегда, пока их не удалят (
deadline_date = null
), либо содержать дату, которая еще не наступила. При этом отмечу, что по истечении даты "протухания" запись из таблицыUsersSegments
автоматически не удаляется — она просто перестает получаться при запросе активных привязок пользователя к сегментам. Запись из таблицы будет удалена при отправке POST-запроса с изменением сегментов, в которых принимает участие пользователь.
Задача | Прогресс | Комментарий |
---|---|---|
Метод создания сегмента | ✅ | |
Метод удаления сегмента | ✅ | |
Метод добавления пользователя в сегмент | ✅ | Добавлена проверка при попытке повторного добавления или удаления сегментов у пользователя — они будут пропускаться |
Метод получения активных сегментов пользователя | ✅ | Так как было выполнено дополнительное задание №2, активными сегментами считаются те, у которых не стоит deadline_date или deadline_date еще не наступил |
Покрытие кода тестами | 🙈 | Были созданы моки, а также добавлено создание БД для тестов, но из-за нехватки времени реализация тестов не была доделана |
Swagger | ✅ | |
Доп. задание №1 (история) | 🙈 | Было реализовано сохранение истории в БД, но осталось нереализованным получение истории в формате отчета |
Доп. задание №2 (дедлайн) | ✅ | Была реализована поддержка установки дедлайна — по истечении срока при запросе активных сегментов пользователя сегмент с просроченным дедлайном возвращаться не будет (но при этом не реализована автоматическая очистка таблицы с сегментами от просроченной поддержки сегмента: необходимо удалять вручную с использованием POST-запроса с изменением сегментов пользователя) |
Доп. задание №3 (seed) | ❌ | |
Запуск в docker | ✅ | |
CI/CD | ✅ | Добавлены этапы Linter, Build и Test в GitHub Actions |
В будущем для улучшения сервиса и работы с ним можно будет сделать следующие улучшения:
- Доделать механизм миграций, которые упростят развертывание базы данных.
- Дописать модульные тесты для валидации работоспособности разработанных модулей.
- Закончить реализацию получения истории изменений сегментов пользователя, добавив метод составления отчета не только в .csv-формате, но и в форматах .docx, .xlsx. Отчеты в данном случае предлагается после генерации хранить в S3-хранилище.
- Добавить стадию паблиша и деплоя в CI/CD для дальнейшего развертывания сервиса.