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

Как выглядит архитектурно последний проект

1.7 Middle🔥 111 комментариев
#Основы Java

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

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

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

Ответ

Архитектура последнего проекта

Мой последний production проект — это E-commerce платформа с микросервисной архитектурой, разработанная в соответствии с лучшими практиками DDD и Clean Architecture.

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                     API Gateway (Kong)                      │
│                  - Rate Limiting                             │
│                  - Authentication                            │
│                  - Request Routing                           │
└──────┬────────┬────────┬────────┬────────────────────────────┘
       │        │        │        │
   ┌───┴──┐ ┌──┴──┐ ┌───┴──┐ ┌──┴────┐
   │Users │ │Order│ │Search│ │Payment│
   │ MS   │ │ MS  │ │  MS  │ │  MS   │
   └───┬──┘ └──┬──┘ └───┬──┘ └──┬────┘
       │      │        │       │
       ↓      ↓        ↓       ↓
    ┌─────────────────────────────────┐
    │   Shared Infrastructure         │
    │  - Message Broker (RabbitMQ)    │
    │  - Cache (Redis)                │
    │  - Service Discovery (Consul)   │
    │  - Logging (ELK Stack)          │
    │  - Monitoring (Prometheus)      │
    └─────────────────────────────────┘

Слоистая архитектура каждого микросервиса

Уровень архитектуры (по слоям):

Presentation Layer
├── REST Controllers
├── Request/Response DTOs
└── Exception Handlers
        ↓
Application Layer
├── UseCases (Orchestration)
├── DTOConverters
└── Validators
        ↓
Domain Layer
├── Entities
├── ValueObjects
├── Domain Services
├── Repository Interfaces
└── Domain Events
        ↓
Infrastructure Layer
├── Repository Implementations (JPA)
├── External Service Clients
├── Event Publishers
└── Database Migrations (Goose)

Структура проекта (Users Microservice пример)

src/main/
├── java/com/example/users/
│   ├── api/
│   │   ├── controller/
│   │   │   ├── UserController.java
│   │   │   └── AuthController.java
│   │   ├── dto/
│   │   │   ├── CreateUserRequest.java
│   │   │   ├── UserResponse.java
│   │   │   └── UpdateUserRequest.java
│   │   └── exception/
│   │       ├── UserNotFoundException.java
│   │       └── GlobalExceptionHandler.java
│   │
│   ├── application/
│   │   ├── service/
│   │   │   ├── CreateUserUseCase.java
│   │   │   ├── UpdateUserUseCase.java
│   │   │   ├── GetUserUseCase.java
│   │   │   └── AuthUseCase.java
│   │   ├── mapper/
│   │   │   └── UserDtoMapper.java
│   │   └── validator/
│   │       └── UserValidator.java
│   │
│   ├── domain/
│   │   ├── entity/
│   │   │   ├── User.java
│   │   │   ├── UserEmail.java (ValueObject)
│   │   │   └── UserRole.java (ValueObject)
│   │   ├── service/
│   │   │   └── PasswordHashingService.java
│   │   ├── event/
│   │   │   └── UserCreatedEvent.java
│   │   ├── repository/
│   │   │   └── UserRepository.java (interface)
│   │   └── exception/
│   │       └── DuplicateEmailException.java
│   │
│   └── infrastructure/
│       ├── persistence/
│       │   ├── JpaUserRepository.java
│       │   ├── UserJpaEntity.java
│       │   └── UserMapper.java
│       ├── external/
│       │   └── EmailServiceClient.java
│       ├── event/
│       │   └── UserEventPublisher.java
│       └── config/
│           ├── JpaConfig.java
│           ├── BeanConfig.java
│           └── SecurityConfig.java
│
└── resources/
    ├── application.yml
    ├── application-dev.yml
    ├── application-prod.yml
    └── db/migration/
        ├── 0001_create_users_table.sql
        ├── 0002_add_email_index.sql
        └── 0003_add_roles_table.sql

Domain Entity (DDD Подход)

@Entity
@Table(name = "users")
public class User {
    @Id
    private UUID id;
    
    @Embedded
    private UserEmail email;  // ValueObject
    
    @Column
    private String passwordHash;
    
    @Column
    private String name;
    
    @ElementCollection
    @CollectionTable(name = "user_roles")
    private Set<UserRole> roles;  // ValueObject
    
    @Column
    private LocalDateTime createdAt;
    
    @Column
    private LocalDateTime updatedAt;
    
    @Column
    private UserStatus status;  // Enum
    
    // Domain Events
    @Transient
    private List<DomainEvent> domainEvents = new ArrayList<>();
    
    // Factory method
    public static User create(String email, String name, String password) {
        User user = new User();
        user.id = UUID.randomUUID();
        user.email = new UserEmail(email);  // Валидация в ValueObject
        user.name = name;
        user.passwordHash = PasswordHasher.hash(password);
        user.roles = Set.of(UserRole.USER);
        user.status = UserStatus.ACTIVE;
        user.createdAt = LocalDateTime.now(UTC);
        user.updatedAt = LocalDateTime.now(UTC);
        
        // Публикуем доменное событие
        user.addDomainEvent(new UserCreatedEvent(user.id, user.email.value()));
        
        return user;
    }
    
    // Business logic
    public void updateEmail(UserEmail newEmail) {
        if (newEmail.equals(this.email)) {
            throw new InvalidEmailException("Email is already in use");
        }
        this.email = newEmail;
        this.updatedAt = LocalDateTime.now(UTC);
        this.addDomainEvent(new UserEmailChangedEvent(this.id, newEmail.value()));
    }
    
    public void assignRole(UserRole role) {
        this.roles.add(role);
        this.addDomainEvent(new UserRoleAssignedEvent(this.id, role));
    }
    
    private void addDomainEvent(DomainEvent event) {
        this.domainEvents.add(event);
    }
    
    public List<DomainEvent> getDomainEvents() {
        return new ArrayList<>(domainEvents);
    }
    
    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

// ValueObject — неизменяемый, никогда не null
@Embeddable
public class UserEmail {
    @Column(name = "email")
    private String value;
    
    public UserEmail(String value) {
        if (!isValid(value)) {
            throw new InvalidEmailException("Invalid email: " + value);
        }
        this.value = value;
    }
    
    private static boolean isValid(String email) {
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
    
    public String value() {
        return value;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserEmail that = (UserEmail) o;
        return value.equals(that.value);
    }
}

UseCase (Application Service)

@Service
@Transactional
public class CreateUserUseCase {
    
    private final UserRepository userRepository;
    private final PasswordHashingService passwordHashingService;
    private final UserEventPublisher eventPublisher;
    private final UserValidator validator;
    
    @Autowired
    public CreateUserUseCase(
        UserRepository userRepository,
        PasswordHashingService passwordHashingService,
        UserEventPublisher eventPublisher,
        UserValidator validator
    ) {
        this.userRepository = userRepository;
        this.passwordHashingService = passwordHashingService;
        this.eventPublisher = eventPublisher;
        this.validator = validator;
    }
    
    public UserResponse execute(CreateUserRequest request) {
        // Валидация
        validator.validate(request);
        
        // Проверка дубликатов
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException("Email already in use");
        }
        
        // Бизнес-логика: создаём пользователя (в Domain)
        User user = User.create(
            request.getEmail(),
            request.getName(),
            request.getPassword()
        );
        
        // Сохраняем
        User savedUser = userRepository.save(user);
        
        // Публикуем события (асинхронно)
        user.getDomainEvents().forEach(eventPublisher::publish);
        user.clearDomainEvents();
        
        // Конвертируем в DTO
        return UserDtoMapper.toResponse(savedUser);
    }
}

REST Controller

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    private final CreateUserUseCase createUserUseCase;
    private final GetUserUseCase getUserUseCase;
    private final UpdateUserUseCase updateUserUseCase;
    
    @PostMapping
    public ResponseEntity<UserResponse> createUser(@RequestBody CreateUserRequest request) {
        UserResponse response = createUserUseCase.execute(request);
        return ResponseEntity.status(201).body(response);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable UUID id) {
        UserResponse response = getUserUseCase.execute(id);
        return ResponseEntity.ok(response);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(
        @PathVariable UUID id,
        @RequestBody UpdateUserRequest request
    ) {
        UserResponse response = updateUserUseCase.execute(id, request);
        return ResponseEntity.ok(response);
    }
}

Event-Driven Communication (между сервисами)

// Domain Event
public class UserCreatedEvent extends DomainEvent {
    private final UUID userId;
    private final String email;
    
    public UserCreatedEvent(UUID userId, String email) {
        this.userId = userId;
        this.email = email;
    }
}

// Publisher
@Component
public class UserEventPublisher {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void publish(DomainEvent event) {
        String json = objectMapper.writeValueAsString(event);
        rabbitTemplate.convertAndSend(
            "users-exchange",
            "user.created",
            json
        );
    }
}

// Subscriber (в другом микросервисе)
@Component
public class UserEventListener {
    
    @RabbitListener(queues = "orders-queue")
    public void onUserCreated(UserCreatedEvent event) {
        // Создаём соответствующий аккаунт в Order Service
        orderService.initializeUserAccount(event.getUserId());
    }
}

Database Schema (Goose migration)

-- migrations/0001_create_users_table.sql
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at DESC);

-- migrations/0002_create_user_roles_table.sql
CREATE TABLE user_roles (
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role VARCHAR(50) NOT NULL,
    PRIMARY KEY (user_id, role)
);

Configuration & Dependency Injection

@Configuration
public class UsersModuleConfig {
    
    @Bean
    public CreateUserUseCase createUserUseCase(
        UserRepository userRepository,
        PasswordHashingService passwordHashingService,
        UserEventPublisher eventPublisher,
        UserValidator validator
    ) {
        return new CreateUserUseCase(
            userRepository,
            passwordHashingService,
            eventPublisher,
            validator
        );
    }
    
    @Bean
    public UserValidator userValidator() {
        return new UserValidator();
    }
    
    @Bean
    public PasswordHashingService passwordHashingService() {
        return new Argon2PasswordHashingService();
    }
}

Testing Strategy

// Unit test Domain Layer
@Test
public void testUserCanBeCreated() {
    User user = User.create("john@example.com", "John", "password");
    
    assertThat(user.getId()).isNotNull();
    assertThat(user.getEmail().value()).isEqualTo("john@example.com");
    assertThat(user.getDomainEvents()).hasSize(1);
}

// Integration test UseCase
@SpringBootTest
public class CreateUserUseCaseIntegrationTest {
    
    @Autowired
    private CreateUserUseCase useCase;
    
    @Autowired
    private UserRepository repository;
    
    @Test
    @Transactional
    public void testCreateUserPersistsData() {
        CreateUserRequest request = new CreateUserRequest(
            "jane@example.com",
            "Jane",
            "password123"
        );
        
        UserResponse response = useCase.execute(request);
        
        User savedUser = repository.findById(response.getId()).orElseThrow();
        assertThat(savedUser.getEmail().value()).isEqualTo("jane@example.com");
    }
}

// E2E test REST API
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerE2ETest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    public void testCreateUserEndpoint() {
        CreateUserRequest request = new CreateUserRequest(
            "test@example.com",
            "Test User",
            "password123"
        );
        
        ResponseEntity<UserResponse> response = restTemplate.postForEntity(
            "/api/v1/users",
            request,
            UserResponse.class
        );
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getId()).isNotNull();
    }
}

Monitoring & Observability

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${app.environment}
  tracing:
    sampling:
      probability: 0.1  # 10% трейсинга

logging:
  level:
    root: INFO
    com.example: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

Key Design Principles

✅ DDD (Domain-Driven Design)
   - Domain Entity со своей бизнес-логикой
   - Domain Events для асинхронной коммуникации
   - Repository interface в domain, реализация в infrastructure

✅ Clean Architecture
   - Четкие слои: Presentation → Application → Domain → Infrastructure
   - Зависимости направлены внутрь (Presentation → Domain)
   - Никакие слои не знают о деталях внешних слоев

✅ SOLID
   - Single Responsibility: каждый класс одна задача
   - Open/Closed: открыт для расширения, закрыт для модификации
   - Liskov Substitution: полиморфизм через интерфейсы
   - Interface Segregation: узкие интерфейсы
   - Dependency Inversion: зависит от интерфейсов, не реализаций

✅ Микросервисная архитектура
   - Независимые базы данных (Database per service)
   - Асинхронная коммуникация через message broker
   - Event-driven для развязывания сервисов

✅ Testing Strategy
   - Unit тесты для Domain Layer (быстрые)
   - Integration тесты для Application Layer
   - E2E тесты для API (Testcontainers для БД)

✅ Production-Ready
   - Logging: ELK Stack
   - Monitoring: Prometheus + Grafana
   - Tracing: Jaeger
   - Health checks: Spring Actuator

Итог

Мой последний проект демонстрирует:

  1. DDD & Clean Architecture — правильное разделение ответственности
  2. Микросервисы — независимые, масштабируемые сервисы
  3. Event-Driven — слабая связанность через события
  4. Production-Ready — мониторинг, логирование, трейсинг
  5. TDD & Coverage — тесты на всех уровнях (90%+)
  6. Goose Migrations — версионированные миграции БД
  7. Security — Spring Security, Password Hashing, API Gateway
Как выглядит архитектурно последний проект | PrepBro