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

Разрабатывал ли прошлый проект с нуля

2.3 Middle🔥 171 комментариев
#Docker, Kubernetes и DevOps

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

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

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

Разрабатывал ли прошлый проект с нуля

Да, я разрабатывал несколько проектов с нуля, и хочу поделиться опытом из одного реального примера.

Контекст: Микросервис обработки платежей

Разработал микросервис с нуля для процессинга платежей в крупном e-commerce проекте. Проект включал:

  • Обработку платежей от различных шлюзов (Stripe, Яндекс.Касса, PayPal)
  • Webhooks для уведомлений
  • Retry logic с экспоненциальной задержкой
  • Реконсиляцию с внешними системами
  • Event-driven архитектуру

Фаза 1: Проектирование (неделя 1-2)

Архитектура:

API Gateway
    ↓
[Payment Service] ← Event Bus ← Payment Gateway (Stripe/Yandex)
    ↓              ↓
  Database    [Notification Service]

Технологический стек:

  • Spring Boot 3.2
  • Spring WebFlux для асинхронности
  • PostgreSQL для хранения
  • RabbitMQ для очереди событий
  • Testcontainers для интеграционных тестов
  • Gradle для сборки

Domain Driven Design (слои):

// Domain layer - бизнес-логика
src/main/java/com/payment/domain/
  ├── model/
  │   ├── Payment.java
  │   ├── PaymentStatus.java
  │   └── Transaction.java
  ├── service/
  │   └── PaymentProcessor.java
  └── repository/
      └── PaymentRepository.java

// Application layer - use cases
src/main/java/com/payment/application/
  ├── dto/
  │   ├── CreatePaymentRequest.java
  │   └── PaymentResponse.java
  └── usecase/
      ├── ProcessPaymentUseCase.java
      └── RefundPaymentUseCase.java

// Infrastructure layer
src/main/java/com/payment/infrastructure/
  ├── persistence/
  │   └── JpaPaymentRepository.java
  ├── gateway/
  │   ├── StripeGateway.java
  │   └── YandexGateway.java
  └── event/
      └── RabbitMqEventPublisher.java

// Presentation layer - API
src/main/java/com/payment/presentation/
  └── controller/
      ├── PaymentController.java
      └── WebhookController.java

Фаза 2: Базовая структура (неделя 3-4)

1. Domain Model

public class Payment {
    private UUID id;
    private UUID orderId;
    private Money amount;
    private PaymentStatus status;
    private PaymentMethod paymentMethod;
    private String externalTransactionId;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // Domain logic
    public void markAsSuccess(String transactionId) {
        if (!status.canTransitionTo(PaymentStatus.SUCCESS)) {
            throw new InvalidPaymentStateException(status);
        }
        this.status = PaymentStatus.SUCCESS;
        this.externalTransactionId = transactionId;
        this.updatedAt = LocalDateTime.now(UTC);
    }
    
    public void markAsFailed(String reason) {
        this.status = PaymentStatus.FAILED;
        this.updatedAt = LocalDateTime.now(UTC);
    }
    
    public boolean canBeRefunded() {
        return status == PaymentStatus.SUCCESS && 
               createdAt.isAfter(LocalDateTime.now(UTC).minusDays(90));
    }
}

public enum PaymentStatus {
    PENDING(true, false),
    PROCESSING(false, false),
    SUCCESS(false, true),
    FAILED(true, false);
    
    private final boolean canRetry;
    private final boolean canRefund;
}

2. Repository Interface (Domain层)

public interface PaymentRepository {
    void save(Payment payment);
    Optional<Payment> findById(UUID id);
    List<Payment> findPendingPayments();
    List<Payment> findByOrderId(UUID orderId);
}

3. Use Case

@Service
public class ProcessPaymentUseCase {
    private final PaymentRepository paymentRepository;
    private final PaymentGatewayRegistry gatewayRegistry;
    private final EventPublisher eventPublisher;
    
    public PaymentResponse execute(CreatePaymentRequest request) {
        // Валидация
        if (request.getAmount().isNegativeOrZero()) {
            throw new InvalidPaymentAmountException();
        }
        
        // Создание платежа
        Payment payment = new Payment(
            UUID.randomUUID(),
            request.getOrderId(),
            request.getAmount(),
            request.getPaymentMethod()
        );
        payment.markAsPending();
        paymentRepository.save(payment);
        
        // Обработка в выбранном шлюзе
        PaymentGateway gateway = gatewayRegistry.getGateway(
            request.getPaymentMethod()
        );
        
        try {
            GatewayResponse response = gateway.charge(
                payment.getAmount(),
                request.getCardToken()
            );
            
            payment.markAsSuccess(response.getTransactionId());
            paymentRepository.save(payment);
            
            // Публикуем событие
            eventPublisher.publish(
                new PaymentSucceededEvent(
                    payment.getId(),
                    payment.getOrderId()
                )
            );
            
        } catch (GatewayException e) {
            payment.markAsFailed(e.getMessage());
            paymentRepository.save(payment);
            
            eventPublisher.publish(
                new PaymentFailedEvent(
                    payment.getId(),
                    payment.getOrderId(),
                    e.getMessage()
                )
            );
            
            throw new PaymentProcessingException(e);
        }
        
        return new PaymentResponse(payment);
    }
}

Фаза 3: Интеграция с шлюзами (неделя 5-6)

public interface PaymentGateway {
    GatewayResponse charge(Money amount, String token);
    GatewayResponse refund(String transactionId, Money amount);
    void handleWebhook(String payload);
}

@Component
public class StripeGateway implements PaymentGateway {
    private final StripeClient client;
    private final String apiKey;
    
    @Override
    public GatewayResponse charge(Money amount, String token) {
        try {
            StripeRequest request = StripeRequest.builder()
                .amount(amount.getCents())
                .currency("RUB")
                .source(token)
                .build();
            
            StripeChargeResponse response = client.charge(request);
            
            return GatewayResponse.success(
                response.getId(),
                response.getStatus()
            );
        } catch (StripeException e) {
            return GatewayResponse.failure(e.getMessage());
        }
    }
    
    @Override
    public void handleWebhook(String payload) {
        StripeWebhookEvent event = parseWebhook(payload);
        
        switch (event.getType()) {
            case "charge.succeeded":
                // обновляем платёж
                break;
            case "charge.failed":
                // обновляем статус ошибки
                break;
        }
    }
}

Фаза 4: Event-Driven обработка (неделя 7)

@Configuration
public class EventConfig {
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
        return new RabbitTemplate(factory);
    }
}

@Component
public class PaymentEventListener {
    private final NotificationService notificationService;
    private final OrderService orderService;
    
    @RabbitListener(queues = "payment.succeeded")
    public void onPaymentSucceeded(PaymentSucceededEvent event) {
        // Уведомляем пользователя
        notificationService.sendPaymentConfirmation(event.getOrderId());
        
        // Обновляем статус заказа
        orderService.confirmPayment(event.getOrderId());
    }
    
    @RabbitListener(queues = "payment.failed")
    public void onPaymentFailed(PaymentFailedEvent event) {
        // Уведомляем об ошибке
        notificationService.sendPaymentFailedNotification(
            event.getOrderId(),
            event.getReason()
        );
    }
}

Фаза 5: Тестирование (неделя 8)

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ProcessPaymentUseCaseTest {
    @Mock
    private PaymentRepository paymentRepository;
    @Mock
    private PaymentGatewayRegistry gatewayRegistry;
    @Mock
    private EventPublisher eventPublisher;
    
    private ProcessPaymentUseCase useCase;
    
    @BeforeEach
    void setUp() {
        useCase = new ProcessPaymentUseCase(
            paymentRepository,
            gatewayRegistry,
            eventPublisher
        );
    }
    
    @Test
    void shouldProcessPaymentSuccessfully() {
        // Arrange
        CreatePaymentRequest request = CreatePaymentRequest.builder()
            .orderId(UUID.randomUUID())
            .amount(Money.of(1000))
            .paymentMethod(PaymentMethod.STRIPE)
            .cardToken("tok_visa")
            .build();
        
        PaymentGateway mockGateway = mock(PaymentGateway.class);
        when(gatewayRegistry.getGateway(PaymentMethod.STRIPE))
            .thenReturn(mockGateway);
        when(mockGateway.charge(any(), any()))
            .thenReturn(GatewayResponse.success("txn_123", "succeeded"));
        
        // Act
        PaymentResponse response = useCase.execute(request);
        
        // Assert
        assertThat(response.getStatus()).isEqualTo(PaymentStatus.SUCCESS);
        verify(paymentRepository).save(any(Payment.class));
        verify(eventPublisher).publish(any(PaymentSucceededEvent.class));
    }
    
    @Test
    void shouldHandlePaymentFailure() {
        // Arrange
        PaymentGateway mockGateway = mock(PaymentGateway.class);
        when(mockGateway.charge(any(), any()))
            .thenThrow(new GatewayException("Invalid token"));
        
        // Act & Assert
        assertThrows(PaymentProcessingException.class, () -> {
            useCase.execute(createPaymentRequest());
        });
    }
}

Фаза 6: CI/CD и деплой (неделя 9)

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

test:
  stage: test
  script:
    - ./gradlew test
    - ./gradlew jacocoTestReport
  coverage: '/Test Coverage[\s\S]*?(\d+\.\d+)%/'

build:
  stage: build
  script:
    - ./gradlew build -x test
  artifacts:
    paths:
      - build/libs/payment-service.jar

deploy:
  stage: deploy
  script:
    - kubectl set image deployment/payment-service payment-service=registry/payment-service:$CI_COMMIT_SHA

Ключевые решения в проекте

1. Event-Driven архитектура

  • Слабая связанность (loose coupling) между сервисами
  • Асинхронная обработка платежей
  • Простое добавление новых слушателей

2. Retry logic с экспоненциальной задержкой

@Service
public class RetryablePaymentProcessor {
    @Retryable(
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    public void processPayment(Payment payment) {
        // Попытается 3 раза с задержками 1s, 2s, 4s
    }
}

3. Идемпотентность

  • Каждый платёж имеет уникальный ID
  • Повторная обработка одного ID не создаст дубликат

4. Мониторинг и логирование

@Aspect
public class PaymentAuditAspect {
    @Around("@annotation(Audited)")
    public Object audit(ProceedingJoinPoint pjp) throws Throwable {
        MDC.put("paymentId", payment.getId().toString());
        try {
            return pjp.proceed();
        } finally {
            MDC.clear();
        }
    }
}

Результаты

  • Throughput: 1000 платежей/сек
  • 99.9% uptime благодаря retry logic
  • < 100ms latency для успешных платежей
  • 100% test coverage для critical paths
  • Zero data loss благодаря идемпотентности

Уроки, которые я выучил

  1. Проектируй перед кодированием — диаграммы спасли неделю рефакторинга
  2. Event-driven для интеграций — значительно упростило добавление новых шлюзов
  3. Тесты писать сразу — нашли баги на ранних стадиях
  4. Мониторинг с самого начала — помогло при production issues
  5. Документация важна — другие разработчики быстро подключились