← Назад к вопросам

Расскажи о крупном проекте над которым работал

1.3 Junior🔥 201 комментариев
#Soft Skills

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Крупный проект: Платформа управления подписками

Контекст

Я был lead backend разработчиком в проекте платформы управления подписками для SaaS компании. Это был high-load проект с 500k+ активных пользователей, обрабатывающий 10k запросов в секунду.

Задача

Построить систему, которая:

  • Обрабатывает платежи от Stripe, Paddle, AppStore
  • Управляет подписками пользователей (создание, продление, отмена)
  • Отправляет уведомления (email, push, webhook)
  • Работает 24/7 с uptime > 99.9%
  • Обеспечивает консистентность данных при сбоях

Архитектура

Стек технологий:

  • Django + Django REST Framework для основного API
  • PostgreSQL для основной БД
  • Redis для кеша и очередей
  • Celery + RabbitMQ для асинхронных задач
  • Docker + Kubernetes для развёртывания
  • Prometheus + Grafana для мониторинга
  • Sentry для отслеживания ошибок

Слои архитектуры (Clean Architecture):

Presentation (API endpoints)
    ↓
Application (Use Cases, Services)
    ↓
Domain (Business Logic, Entities)
    ↓
Infrastructure (DB, External APIs)

Основные компоненты

1. Payment Service (обработка платежей)

# domain/payment/entities.py
class Payment(AggregateRoot):
    id: UUID
    subscription_id: UUID
    amount: Decimal
    currency: str
    status: PaymentStatus  # pending, completed, failed
    payment_provider: str  # stripe, paddle, appstore
    external_id: str  # ID в системе провайдера
    created_at: datetime
    
    def mark_completed(self, transaction_id: str):
        self.status = PaymentStatus.COMPLETED
        self.external_id = transaction_id
        self.publish(PaymentCompletedEvent(self.id))
    
    def mark_failed(self, reason: str):
        self.status = PaymentStatus.FAILED
        self.publish(PaymentFailedEvent(self.id, reason))

# application/payment/use_cases/process_payment.py
class ProcessPaymentUseCase:
    def __init__(self, payment_repo: PaymentRepository,
                 stripe_service: StripeService,
                 event_bus: EventBus):
        self.payment_repo = payment_repo
        self.stripe_service = stripe_service
        self.event_bus = event_bus
    
    async def execute(self, payment_id: UUID) -> Result:
        payment = await self.payment_repo.get(payment_id)
        
        try:
            # Обрабатываем платёж
            result = await self.stripe_service.charge(
                amount=payment.amount,
                currency=payment.currency,
                customer_id=payment.customer_id
            )
            
            # Обновляем платёж
            payment.mark_completed(result.transaction_id)
            await self.payment_repo.save(payment)
            
            # Публикуем события
            for event in payment.events:
                await self.event_bus.publish(event)
            
            return Result.ok(payment)
        
        except StripeError as e:
            payment.mark_failed(str(e))
            await self.payment_repo.save(payment)
            return Result.fail(str(e))

2. Subscription Service (управление подписками)

# domain/subscription/entities.py
class Subscription(AggregateRoot):
    id: UUID
    user_id: UUID
    plan_id: UUID
    status: SubscriptionStatus  # active, paused, cancelled
    billing_period: BillingPeriod  # monthly, yearly
    current_period_start: date
    current_period_end: date
    renewal_count: int = 0
    cancelled_at: datetime | None = None
    
    def activate(self):
        self.status = SubscriptionStatus.ACTIVE
        self.publish(SubscriptionActivatedEvent(self.id))
    
    def pause(self):
        self.status = SubscriptionStatus.PAUSED
        self.publish(SubscriptionPausedEvent(self.id))
    
    def cancel(self, reason: str = ''):
        self.status = SubscriptionStatus.CANCELLED
        self.cancelled_at = datetime.now(UTC)
        self.publish(SubscriptionCancelledEvent(self.id, reason))
    
    def needs_renewal(self) -> bool:
        return self.current_period_end <= date.today()

# application/subscription/use_cases/renew_subscription.py
class RenewSubscriptionUseCase:
    def __init__(self, subscription_repo: SubscriptionRepository,
                 payment_service: PaymentService,
                 event_bus: EventBus,
                 logger: Logger):
        self.subscription_repo = subscription_repo
        self.payment_service = payment_service
        self.event_bus = event_bus
        self.logger = logger
    
    async def execute(self, subscription_id: UUID) -> Result:
        async with transaction.atomic():
            # Заблокируем подписку для других операций
            sub = await self.subscription_repo.get_for_update(subscription_id)
            
            if not sub.needs_renewal():
                return Result.ok(sub)
            
            # Создаём платёж
            plan = await self.get_plan(sub.plan_id)
            payment = Payment(
                subscription_id=sub.id,
                amount=plan.price,
                currency=plan.currency,
                payment_provider=sub.payment_provider
            )
            
            # Обрабатываем платёж
            result = await self.payment_service.process(payment)
            
            if result.is_ok():
                # Обновляем подписку
                sub.renewal_count += 1
                sub.current_period_start = date.today()
                sub.current_period_end = date.today() + timedelta(
                    days=365 if sub.billing_period == 'yearly' else 30
                )
                sub.publish(SubscriptionRenewedEvent(sub.id))
            else:
                self.logger.error(f"Payment failed for subscription {sub.id}")
                # Отправляем уведомление пользователю
                sub.publish(SubscriptionRenewalFailedEvent(sub.id))
            
            await self.subscription_repo.save(sub)
            return result

3. Webhook обработчик

# presentation/api/webhooks.py
@router.post("/webhooks/stripe")
async def stripe_webhook(request: Request,
                         stripe_service: StripeService = Depends()):
    # Верифицируем подпись
    payload = await request.body()
    signature = request.headers.get('Stripe-Signature')
    
    event = stripe_service.verify_webhook(payload, signature)
    
    # Отправляем на обработку через Celery
    process_stripe_event.delay(event)
    
    return {"ok": True}

# tasks/webhooks.py
@app.task(bind=True, max_retries=3)
def process_stripe_event(self, event: dict):
    try:
        event_type = event['type']
        data = event['data']['object']
        
        handlers = {
            'payment_intent.succeeded': handle_payment_succeeded,
            'payment_intent.payment_failed': handle_payment_failed,
            'charge.refunded': handle_refund,
        }
        
        handler = handlers.get(event_type)
        if handler:
            handler(data)
    
    except Exception as exc:
        raise self.retry(exc=exc, countdown=2 ** self.request.retries)

Вызовы и решения

1. Идемпотентность при сбоях

Проблема: Если webhook пришёл дважды, платёж обработается дважды.

Решение:

class IdempotencyService:
    async def process(self, key: str, operation, *args, **kwargs):
        # Проверяем, обработали ли уже
        result = await self.cache.get(f"idempotency:{key}")
        if result:
            return result
        
        # Если нет, выполняем операцию
        result = await operation(*args, **kwargs)
        
        # Кешируем результат на час
        await self.cache.set(f"idempotency:{key}", result, ttl=3600)
        return result

# Использование
await idempotency_service.process(
    key=f"payment_{payment_id}",
    operation=process_payment_use_case.execute,
    payment_id=payment_id
)

2. Консистентность данных

Проблема: При сбое сервера платёж обработан, но подписка не обновлена.

Решение: Transactional Outbox Pattern

class SubscriptionService:
    async def renew(self, sub_id: UUID):
        async with transaction.atomic():
            # Обновляем подписку в одной транзакции
            sub = await Subscription.objects.select_for_update().get(id=sub_id)
            sub.current_period_end = date.today() + timedelta(days=30)
            await sub.save()
            
            # Записываем события в ту же транзакцию
            await OutboxEvent.objects.create(
                event_type='subscription.renewed',
                data={'subscription_id': str(sub.id)}
            )
            # После commit все события отправятся асинхронно

3. Масштабирование

Проблема: 10k запросов/сек требует кеширования и оптимизации.

Решение:

class SubscriptionCache:
    async def get(self, user_id: UUID) -> Subscription:
        # Кешируем в Redis на 5 минут
        cache_key = f"subscription:{user_id}"
        cached = await self.redis.get(cache_key)
        
        if cached:
            return Subscription.parse_raw(cached)
        
        # Если не в кеше, читаем из БД
        sub = await Subscription.objects.get(user_id=user_id)
        
        # Кешируем результат
        await self.redis.setex(
            cache_key,
            300,  # 5 минут
            sub.json()
        )
        
        return sub
    
    async def invalidate(self, user_id: UUID):
        await self.redis.delete(f"subscription:{user_id}")

# Индекс в БД
class Subscription(models.Model):
    class Meta:
        indexes = [
            models.Index(fields=['user_id', 'status']),
            models.Index(
                fields=['current_period_end'],
                condition=Q(status='active'),
                name='idx_active_subs_renew'
            ),
        ]

Результаты

  • Обработка 10k запросов/сек с latency < 100ms
  • Uptime 99.95% (4 часа downtime в год)
  • Корректная обработка 500k пользователей
  • Автоматическое восстановление при сбоях
  • Zero дублирования платежей (идемпотентность)
  • 90%+ test coverage

Ключевые уроки

  1. DDD и Clean Architecture работают отлично при высокой нагрузке
  2. Асинхронная обработка (Celery) критична для масштабирования
  3. Кеширование требуется для высокой пропускной способности
  4. Идемпотентность необходима при работе с внешними системами
  5. Мониторинг и logging помогают находить проблемы быстро
  6. Транзакции и SELECT FOR UPDATE предотвращают race conditions

Этот проект научил меня строить действительно надёжные системы, которые работают в production.