REST API для управления кошельками с поддержкой идемпотентности и защитой от race conditions.
- Скопируйте
.env.exampleв.envи при необходимости измените доступы к БД:
cp .env.example .env
.envсодержит секреты и не попадает в git (см..gitignore).
- Запустите приложение:
docker-compose up -dПриложение будет доступно на http://localhost:8000.
| Метод | Путь | Idempotency-Key | Описание |
|---|---|---|---|
GET |
/health |
❌ | Healthcheck |
POST |
/api/v1/wallets |
✅ | Создать кошелёк |
POST |
/api/v1/wallets/{id}/deposit |
✅ | Пополнить кошелёк |
POST |
/api/v1/wallets/transfer |
✅ | Перевести средства |
GET |
/api/v1/wallets/{id}?limit=50 |
❌ | Баланс и история операций |
# Создать кошелёк
curl -X POST http://localhost:8000/api/v1/wallets \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{"metadata": {"owner": "Alice"}}'
# Пополнить кошелёк
curl -X POST http://localhost:8000/api/v1/wallets/<wallet_id>/deposit \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{"amount": "100.00"}'
# Перевести средства
curl -X POST http://localhost:8000/api/v1/wallets/transfer \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440002" \
-d '{"from_wallet_id": "<id>", "to_wallet_id": "<id>", "amount": "30.00"}'
# Получить баланс и историю
curl http://localhost:8000/api/v1/wallets/<wallet_id>?limit=10# Убедитесь, что PostgreSQL запущен и доступен
docker-compose up -d db
# Скопируйте .env (если ещё не сделали этого)
cp .env.example .env
# Установите зависимости
pip install -r requirements.txt
# Запустите тесты
pytest -vПроблема: два одновременных запроса на перевод с одного кошелька могут прочитать баланс 100, проверить, что 100 >= 100, и оба выполнить списание, уйдя в минус.
Решение: пессимистичная блокировка строк (SELECT ... FOR UPDATE).
- При операциях с балансом (deposit, transfer) все затрагиваемые кошельки блокируются в одной транзакции с помощью
SELECT ... FOR UPDATE. - Кошельки блокируются в порядке
ORDER BY id— это гарантирует отсутствие deadlock'ов. - Второй запрос ждёт завершения первого, видит уже обновлённый баланс и корректно обрабатывает ситуацию (недостаточно средств).
Проблема: клиент отправляет перевод, не дожидается ответа из-за обрыва сети и отправляет тот же запрос снова. Деньги могут списаться дважды.
Решение: заголовок Idempotency-Key.
- Все mutation-эндпоинты требуют заголовка
Idempotency-Key. - Перед выполнением операции проверяется, не было ли уже обработано такого же ключа.
- Если ключ уже есть и тело запроса совпадает — возвращается закешированный ответ.
- Если ключ уже есть, но тело отличается —
409 Conflict. - Idempotency-ключ сохраняется в той же транзакции, что и операция. Если два concurrent запроса с одинаковым ключом,
UNIQUE-constraint базы данных гарантирует, что операция выполнится только один раз.
- Все денежные суммы хранятся в
NUMERIC(20, 2)в PostgreSQL. - В Python используется
Decimal. - В API суммы передаются строками (например,
"100.00"), чтобы избежать потери точности при сериализации JSON.
- FastAPI — асинхронный веб-фреймворк.
- SQLAlchemy 2.0 (async) — ORM.
- asyncpg — драйвер PostgreSQL.
- Alembic — миграции.
- Pytest + pytest-asyncio + httpx — тестирование.
- Docker Compose — инфраструктура.
app/
├── models/ # ORM-модели (Wallet, Transaction, IdempotencyKey)
├── schemas/ # Pydantic-схемы (запросы/ответы)
├── repositories/ # Доступ к данным (изоляция SQL)
├── services/ # Бизнес-логика (WalletService, IdempotencyService)
├── routers/ # FastAPI-эндпоинты
├── main.py # Точка входа
├── database.py # Engine, session, Base
├── config.py # Конфигурация
└── exceptions.py # Кастомные исключения
Слои:
- Роутер — валидация запроса, проверка идемпотентности, вызов сервиса.
- Сервис — бизнес-логика, управление транзакциями.
- Репозиторий — SQL-запросы.
| Ситуация | Статус |
|---|---|
| Кошелёк не найден | 404 |
| Недостаточно средств | 400 |
| Невалидная сумма (<= 0) | 422 |
| Idempotency-Key отсутствует | 422 |
| Idempotency-Key + другое тело | 409 |
| Перевод самому себе | 400 |