← Назад к вопросам
Разрабатывал ли прошлый проект с нуля
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 благодаря идемпотентности
Уроки, которые я выучил
- Проектируй перед кодированием — диаграммы спасли неделю рефакторинга
- Event-driven для интеграций — значительно упростило добавление новых шлюзов
- Тесты писать сразу — нашли баги на ранних стадиях
- Мониторинг с самого начала — помогло при production issues
- Документация важна — другие разработчики быстро подключились