Слон цвета #FF5733: как мы собирали странный, но настоящий e-commerce
Как мы сделали Elephant Color Shop: уникальные цвета, оплата, фоновые задачи, подарки и нормальный прод.
Я люблю задачи, которые на первом созвоне звучат немного нелепо.
«Давайте сделаем магазин слонов. Каждый слон одного уникального цвета. Цвет купили — всё, больше такого ни у кого нет».
Можно отмахнуться: смешная витрина, пара форм, картинка, кнопка «купить». Но если присмотреться, там сразу торчит нормальный продукт. Уникальность товара. Деньги. Асинхронная генерация. Подарки. Пользователи. Продакшен. Всё то, на чём игрушечные проекты обычно и разваливаются.
Поэтому Elephant Color Shop мы делали не как шутку на вечер, а как маленький e-commerce с настоящими правилами. Просто товар у него не кроссовки и не подписка, а слон цвета #FF5733.
Цвет — это не поле в форме
Главное правило простое: один HEX можно купить только один раз.
На словах звучит легко. На практике это место, где нельзя надеяться на «мы перед сохранением проверим». Два человека могут выбрать один цвет почти одновременно. Worker может повторить задачу. Webhook может прийти не тогда, когда ты мысленно нарисовал себе идеальную схему.
Поэтому уникальность лежит там, где ей и место: в PostgreSQL. UniqueConstraint на color_hex — не украшение модели, а последний замок на двери. Всё выше может быть удобным, красивым, дружелюбным. Но база должна иметь право сказать: нет, этот цвет уже занят.
Это тот случай, когда сильное решение выглядит скучно. И нормально. В разработке вообще много хороших решений выглядят скучно, пока они не спасают тебе релиз.
Мы не стали изображать Netflix
Elephant Color Shop — Django-монолит.
Не потому что мы не умеем сложнее. Потому что здесь сложнее было бы хуже.
Проекту нужны аккаунты, заказы, оплата, генерация слонов, подарочные ссылки и API. Всё это отлично живёт в одном приложении, если внутри не устраивать свалку. Мы разнесли домены по Django apps: accounts, payments, elephants, gifts, core. У каждого свой участок, свои модели, свои сервисы, свои API-роутеры.
Такой монолит быстро разрабатывается, понятно деплоится и нормально читается через полгода. А это важнее, чем гордо сказать «у нас сервисная архитектура» и потом неделю искать, почему заказ оплатился, а слон не появился.
Деньги меняют отношение к коду
Пока на сайте нет оплаты, многие ошибки выглядят терпимо. Ну не сгенерировалась картинка. Ну не тот статус. Ну пользователь обновил страницу.
Когда появляется YooKassa, такие «ну» заканчиваются.
Заказ нельзя считать оплаченным потому, что человек нажал кнопку. Оплата подтверждается событием от провайдера. Webhook пришёл — проверили заказ, проверили состояние, прошли критичный участок в транзакции, запустили дальнейшую работу. Не пришёл — не фантазируем.
Это не паранойя. Это нормальная гигиена e-commerce. Деньги не любят код, который «обычно работает».
Слона нельзя рисовать в запросе
После оплаты надо взять SVG-шаблон, покрасить его в нужный цвет и собрать PNG. Можно сделать это прямо в HTTP-запросе. Можно ещё много чего сделать прямо в запросе, если хочется однажды получить висящую страницу, непонятный таймаут и злой лог.
Мы вынесли генерацию в Celery. HTTP быстро отвечает, Redis передаёт задачу, worker спокойно рендерит PNG через CairoSVG и Pillow, заказ меняет статус уже после результата.
Здесь нет магии. Есть граница ответственности. Запрос — это запрос. Генерация файла — фоновая работа. Состояние — в базе. Если задача упала, её можно повторить и нормально разобрать, а не гадать, что видел пользователь в момент таймаута.
Подарки быстро перестают быть милой кнопкой
Подарочная ссылка сначала кажется простой: «дать другу слона». Потом открываешь список вопросов.
Кто владелец сейчас? Можно ли подарить уже подаренного? Что видит человек без аккаунта? Можно ли принять свой же подарок с другой вкладки? Что произойдёт с правами после принятия?
В итоге подарок стал отдельным доменом, а не полем gifted = true. У ссылки UUID, у принятия подарка свои проверки, у передачи владения — явная бизнес-операция. Это не усложнение ради красоты. Это способ не потерять владельца ресурса в момент, когда «маленькая фича» начинает жить своей жизнью.
Фронтенд без лишнего веса
Для интерфейса мы не брали тяжёлый SPA-стек. Django templates, Alpine.js, DaisyUI, Tailwind — достаточно.
Это не «экономия на фронтенде». Это трезвый выбор под продукт. Формы, статусы, личный кабинет, подарочная страница, API-доки — всё это не требует отдельного фронтенд-приложения только ради ощущения масштаба.
Хорошая разработка — это не всегда добавить технологию. Часто это вовремя её не добавить.
Прод — часть разработки, а не приложение к ней
Проект работает не в README. Он работает в контейнерах.
В Docker подняты web, Celery worker, PostgreSQL и Redis. В проде всё стоит за shared Traefik: маршрутизация, TLS, нормальный вход в инфраструктуру. Есть health checks, логи, backup/restore-скрипты, deploy checklist, GitHub Actions.
Я не очень верю в «мы код написали, дальше пусть админ разбирается». Если продукт нельзя поднять, проверить, откатить и восстановить, он ещё не готов. Особенно когда внутри платежи и пользовательские покупки.
Что в итоге
Получился странный на вид, но вполне настоящий магазин:
- регистрация по email и Google OAuth;
- два тарифа: случайный цвет или выбор HEX/оттенка;
- YooKassa и webhook-обработка;
- уникальность цвета на уровне PostgreSQL;
- генерация PNG в Celery;
- подарочные UUID-ссылки;
- API на Django Ninja;
- деплой через Docker и Traefik.
Мне нравится этот проект именно из-за контраста. Снаружи — слон уникального цвета. Внутри — все обычные вопросы веб-продукта: где правда о состоянии, кто владелец, что делать с деньгами, где граница фоновой задачи, как не сломать прод.
И это хороший формат для нас. Берём идею без лишнего пафоса, разбираем её до правил, собираем продукт, поднимаем инфраструктуру и отвечаем за то, чтобы оно жило не только на демо.
Мы это пишем не потому, что «кейсы повышают доверие». Мы это пишем, потому что так и работаем.