Розробка бекенду сайту на Java (Spring Boot)
Spring Boot — промисловий стандарт для Java-бекенду. Це не фреймворк у звичайному сенсі, а конфігураційний шар поверх Spring Framework, який усуває XML-конфігурацію і «вгадує» необхідні налаштування на основі залежностей у classpath. Результат: вбудований Tomcat/Undertow, автоконфігурація JPA, Security, Actuator — все готово до роботи після додання стартерів.
Spring Boot вибирають для: enterprise-систем з багаторічною підтримкою, проектів, де потрібна зріла екосистема транзакцій і безпеки, команд з наявною Java-експертизою, інтеграцій з legacy Java-системами.
Структура проекту
src/main/java/com/myapp/
Application.java # точка входу
config/
SecurityConfig.java
SwaggerConfig.java
CacheConfig.java
domain/
product/
Product.java # JPA Entity
ProductRepository.java # Spring Data
ProductService.java
ProductController.java
dto/
CreateProductRequest.java
ProductResponse.java
domain/
user/
order/
infrastructure/
persistence/
messaging/
external/
exception/
GlobalExceptionHandler.java
src/main/resources/
application.yml
application-prod.yml
Entity та JPA
@Entity
@Table(name = "products",
indexes = {
@Index(name = "idx_products_slug", columnList = "slug", unique = true),
@Index(name = "idx_products_cat_active", columnList = "category_id, is_active")
})
@EntityListeners(AuditingEntityListener.class)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
@Column(unique = true, length = 255)
private String slug;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@Column(columnDefinition = "jsonb")
@Convert(converter = JsonAttributeConverter.class)
private Map<String, Object> attributes = new HashMap<>();
@Column(nullable = false)
private boolean isActive = true;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
// getters/setters або @Data Lombok
}
Repository
Spring Data JPA генерує реалізацію із сигнатури методу:
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findByIsActiveTrueOrderByCreatedAtDesc(Pageable pageable);
Page<Product> findByCategoryIdAndIsActiveTrueOrderByCreatedAtDesc(
Long categoryId, Pageable pageable);
Optional<Product> findBySlug(String slug);
@Query("""
SELECT p FROM Product p
LEFT JOIN FETCH p.category
WHERE p.isActive = true
AND (:search IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :search, '%')))
""")
Page<Product> searchActive(@Param("search") String search, Pageable pageable);
// Projection для легковажних запитів
@Query("SELECT p.id as id, p.name as name, p.price as price FROM Product p WHERE p.isActive = true")
List<ProductSummary> findAllSummaries();
}
Service
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final ApplicationEventPublisher eventPublisher;
private final CacheManager cacheManager;
public Page<ProductResponse> findAll(ProductListRequest request) {
Pageable pageable = PageRequest.of(
request.page(),
request.limit(),
Sort.by(Sort.Direction.DESC, "createdAt")
);
Page<Product> page = (request.search() != null)
? productRepository.searchActive(request.search(), pageable)
: productRepository.findByIsActiveTrueOrderByCreatedAtDesc(pageable);
return page.map(ProductResponse::from);
}
@Transactional
@CacheEvict(value = "products", allEntries = true)
public ProductResponse create(CreateProductRequest request) {
Category category = request.categoryId() != null
? categoryRepository.findById(request.categoryId())
.orElseThrow(() -> new EntityNotFoundException("Category not found"))
: null;
Product product = new Product();
product.setName(request.name());
product.setSlug(SlugUtils.generate(request.name()));
product.setPrice(request.price());
product.setCategory(category);
product = productRepository.save(product);
eventPublisher.publishEvent(new ProductCreatedEvent(product));
return ProductResponse.from(product);
}
}
Controller
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Tag(name = "Products", description = "Product management API")
public class ProductController {
private final ProductService productService;
@GetMapping
public ResponseEntity<Page<ProductResponse>> list(
@Valid ProductListRequest request) {
return ResponseEntity.ok(productService.findAll(request));
}
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> get(@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@ResponseStatus(HttpStatus.CREATED)
public ProductResponse create(@Valid @RequestBody CreateProductRequest request) {
return productService.create(request);
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ProductResponse update(
@PathVariable Long id,
@Valid @RequestBody UpdateProductRequest request) {
return productService.update(id, request);
}
}
DTO з валідацією
public record CreateProductRequest(
@NotBlank @Size(min = 2, max = 255)
String name,
@NotNull @Positive
BigDecimal price,
@Positive
Long categoryId,
@Size(max = 5000)
String description
) {}
public record ProductResponse(
Long id,
String name,
String slug,
BigDecimal price,
CategoryDto category,
Instant createdAt
) {
public static ProductResponse from(Product p) {
return new ProductResponse(
p.getId(), p.getName(), p.getSlug(), p.getPrice(),
p.getCategory() != null ? CategoryDto.from(p.getCategory()) : null,
p.getCreatedAt()
);
}
}
Безпека
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers(GET, "/api/v1/products/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> e
.authenticationEntryPoint((req, res, ex) ->
res.sendError(401, "Unauthorized"))
)
.build();
}
}
Асинхронність та події
@Async
@TransactionalEventListener(phase = AFTER_COMMIT)
public void handleProductCreated(ProductCreatedEvent event) {
notificationService.notifyAdmins("New product: " + event.product().getName());
searchIndexer.index(event.product());
}
// Заплановані задачі
@Scheduled(cron = "0 0 3 * * *", zone = "Europe/Moscow")
public void cleanupExpiredSessions() {
sessionRepository.deleteExpired(Instant.now().minus(Duration.ofDays(30)));
}
Кеширування
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public ProductResponse findById(Long id) {
return productRepository.findById(id)
.map(ProductResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Product " + id));
}
@CacheEvict(value = "products", key = "#id")
@Transactional
public void delete(Long id) {
productRepository.deleteById(id);
}
}
Actuator та моніторинг
Spring Boot Actuator надає health-check, метрики Micrometer, endpoints для Prometheus:
# application.yml
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus, info
endpoint:
health:
show-details: when-authorized
metrics:
export:
prometheus:
enabled: true
Графік розробки
Spring Boot вимагає часу на налаштування, але надає зрілу інфраструктуру:
- Архітектура + Spring scaffold — 1 тиждень
- Entities + JPA + Flyway міграції — 1 тиждень
- Controllers + Services + Security — 2–3 тижні
- Тести (JUnit 5 + MockMvc + Testcontainers) — 1–2 тижні
- Інтеграції + Kafka/RabbitMQ — 1–2 тижні
Корпоративний бекенд середнього масштабу: 8–16 тижнів. Spring Boot — правильний вибір, коли потрібна зрілість екосистеми, строга типізація і команда з Java-опитом.







