Опирается на правила: JS-6.1JS-6.8 и JS-6.X1JS-6.X2 из Java Style Guide → раздел 6. Lombok.

Важно знать

  • @RequiredArgsConstructor — всегда для классов с private final полями (Spring beans, services, exceptions, value-objects).
  • @Slf4j — вместо ручного LoggerFactory.getLogger.
  • @Getter на custom exceptions и value-objects (не record).
  • Lombok НЕ на records — record уже даёт ctor, accessors, equals, hashCode, toString.
  • @Data ЗАПРЕЩЁН в production — mutable setters + equals/hashCode по всем полям.
  • @Builder точечно — сложные DTO 5+ полей; запрещён на aggregate root.
  • @Getter/@Setter для mutable классов где record неприменим (jOOQ POJO, JPA entity).
  • @AllArgsConstructor запрещён для DI-классов — non-final поля попадают в ctor.

Lombok устраняет boilerplate без изменения семантики кода. UCP формулирует обязательные defaults: каждый класс с final полями использует @RequiredArgsConstructor, каждый класс с логированием — @Slf4j. Не «можно использовать», а «обязан использовать». Запреты — на @Data и неаккуратные @AllArgsConstructor.

@RequiredArgsConstructor — всегда

JS-6.1: универсальное правило.

// Spring bean
@Component
@RequiredArgsConstructor
public class CreateOrderHandler implements UseCaseHandler<CreateOrderCommand, Order> {
    private final OrderRepository orderRepository;
    private final DateTimeService dateTimeService;
    private final UuidGenerator uuidGenerator;
}

// Не Spring bean — те же правила
@RequiredArgsConstructor
public class OrderTotalCalculator {
    private final TaxService taxService;
    private final DiscountPolicy discountPolicy;

    public Money calculate(List<OrderItem> items) { ... }
}

// Custom exception с payload
@Getter
@RequiredArgsConstructor
public class InvalidStateTransitionException extends RuntimeException {
    private final ProductStatus from;
    private final ProductStatus to;
}

Lombok генерирует:

public CreateOrderHandler(OrderRepository orderRepository, DateTimeService dateTimeService, UuidGenerator uuidGenerator) {
    this.orderRepository = orderRepository;
    this.dateTimeService = dateTimeService;
    this.uuidGenerator = uuidGenerator;
}

Применяется ко всем private final полям. Optional fields (не final, не required) — игнорируются.

Когда явный ctor оправдан

  1. Валидация аргументовObjects.requireNonNull, if (x < 0) throw ....
  2. super(...) с не-стандартными аргументами.
  3. Factory-method: Order.draft(...), Order.fromPersistence(...) — это уже named-constructor.
  4. No-arg ctor для JPA/Jackson@NoArgsConstructor(access = PROTECTED) + @Getter + @Setter.
public CreateOrderHandler(OrderRepository orderRepository, DateTimeService dateTimeService) {
    this.orderRepository = Objects.requireNonNull(orderRepository, "orderRepository");
    this.dateTimeService = Objects.requireNonNull(dateTimeService, "dateTimeService");
}

С Spring @Autowired и не-null validation — обычно не нужно (Spring сам бросает NoSuchBeanDefinitionException).

@Slf4j

JS-6.2: вместо ручного логгера.

// ✓
@Slf4j
@Component
public class OrderService {
    public void process(Order order) {
        log.info("Processing order: orderId={}", order.id());
    }
}

// ✗ — boilerplate
@Component
public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void process(Order order) {
        log.info("Processing order: orderId={}", order.id());
    }
}

@Slf4j генерирует private static final org.slf4j.Logger log = LoggerFactory.getLogger(ClassName.class). Без boilerplate, без ошибки в class-аргументе (getLogger(OtherClass.class)).

См. также Observability → logging.

@Getter

JS-6.3: для exception payload и value-objects.

@Getter
@RequiredArgsConstructor
public class InvalidStateTransitionException extends RuntimeException {
    private final ProductStatus from;
    private final ProductStatus to;

    @Override
    public String getMessage() {
        return "Invalid transition: " + from + " -> " + to;
    }
}

@Getter генерирует:

public ProductStatus getFrom() { return from; }
public ProductStatus getTo() { return to; }

from, to — payload exception-а, нужны handler-у в GlobalExceptionHandler для построения ProblemDetails. Без @Getter пришлось бы писать accessor-ы руками.

Lombok НЕ на records

JS-6.4: record уже всё генерирует.

// ✓ — обычный record
public record OrderCreatedEvent(
    UUID eventId,
    Long orderId,
    Money amount,
    Instant occurredAt
) {}

// ✗ — Lombok поверх record (компилятор ругается)
@Value
public record OrderCreatedEvent(UUID eventId, Long orderId) {}

@AllArgsConstructor
public record OrderCreatedEvent(UUID eventId, Long orderId) {}

Record даёт автоматически:

  • Constructor (canonical).
  • Accessors eventId(), orderId() (без get-префикса).
  • equals(), hashCode(), toString().

Lombok-аннотации избыточны. Если нужно custom-behavior — compact ctor:

public record OrderCreatedEvent(UUID eventId, Long orderId) {
    public OrderCreatedEvent {
        Objects.requireNonNull(eventId, "eventId");
        Objects.requireNonNull(orderId, "orderId");
    }
}

@Data ЗАПРЕЩЁН

JS-6.5: critical.

// КАТАСТРОФА
@Data
public class OrderEntity {
    private Long id;
    private OrderStatus status;
    private List<OrderItem> items;
}

@Data генерирует:

  • @Getter + @Setter на все поля → mutable, ломает immutability.
  • @EqualsAndHashCode на все поля → ad-hoc equality (любое изменение поля = другой object в Set/Map).
  • @ToString → может logging PII (см. Auth → PII).
  • @RequiredArgsConstructor — generates ctor только на final fields.

Что ломается:

  • JPA entity с @Dataequals() по всем полям, включая lazy-loaded collections → загружает их при сравнении. Performance hit.
  • Set<OrderEntity> — два orders считаются разными если изменён status.
  • PII утечка@ToString включает все поля по умолчанию.

Корректно:

// Immutable value object
public record Order(Long id, OrderStatus status, List<OrderItem> items) {}

// JPA entity — явные @Getter/@Setter
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderEntity {
    @Id
    private Long id;
    private OrderStatus status;
}

Custom equals/hashCode если нужны — пишем руками по id.

@Builder точечно

JS-6.7: не везде.

// ✓ — сложный outgoing API request
@Builder
public record CreatePaymentRequest(
    String idempotencyKey,
    UUID orderId,
    Money amount,
    String currency,
    PaymentMethod method,
    Map<String, String> metadata,
    @Nullable String description,
    @Nullable Instant expirationDate
) {}

// usage
var request = CreatePaymentRequest.builder()
    .idempotencyKey(idempotencyKey)
    .orderId(order.id())
    .amount(order.totalAmount())
    .currency("RUB")
    .method(PaymentMethod.CARD)
    .build();

Когда @Builder уместен:

  • 5+ полей в DTO.
  • Опциональные поля (@Nullable).
  • Сложная подготовка (через method chains).

Когда запрещён:

  • На entity / aggregate root — построение агрегата через named-конструкторы (Order.draft(...), Order.fromPersistence(...)); Lombok-builder обходит инварианты.
  • На простых POJOs с 2-3 полями — overkill.

@Getter/@Setter для mutable

JS-6.8: где record неприменим.

Применяется к:

  • jOOQ POJOs (auto-generated, mutable обязательны).
  • JPA entity (нужен no-arg ctor и setters).
  • Mutable DTOs для draft-объектов, форм.
// jOOQ generated — mutable
@Getter
@Setter
public class OrdersPojo {
    private Long id;
    private OrderStatus status;
}

// JPA entity
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderEntity {
    @Id
    private Long id;
    private OrderStatus status;
}

// Point-by-point — setter только где можно менять
@Getter
public class DraftOrder {
    private final Long id;          // только getter (final)

    @Setter
    private DeliveryAddress address; // setter точечно
}

Иерархия выбора:

  1. record для immutable value object без поведения.
  2. @Value (Lombok) — immutable с методами или сложным constructor.
  3. @Getter без @Setter — final поля, наследование (record неприменим).
  4. @Getter + @Setter — mutable (entity, draft, jOOQ POJO).
  5. Ручные accessors — только при наличии логики.

Build settings

JS-6.6: одинаково везде.

// build.gradle.kts
dependencies {
    compileOnly("org.projectlombok:lombok:1.18.34")
    annotationProcessor("org.projectlombok:lombok:1.18.34")
    testCompileOnly("org.projectlombok:lombok:1.18.34")
    testAnnotationProcessor("org.projectlombok:lombok:1.18.34")
}

compileOnly (не implementation) — Lombok работает на этапе компиляции, в runtime classpath его быть не должно.

Что запрещено

АнтипаттернПравилоЧто взамен
Явный all-args ctor для DIJS-6.X1@RequiredArgsConstructor
@AllArgsConstructor на DI-классеJS-6.X2@RequiredArgsConstructor (только final)
LoggerFactory.getLogger ручнойJS-6.2@Slf4j
@Data в productionJS-6.5record или @Getter+@Setter явно
Lombok поверх recordJS-6.4record даёт всё сам
@Builder на aggregate rootJS-6.7named-конструкторы (Order.draft(...))
Lombok в implementation (не compileOnly)JS-6.6compileOnly + annotationProcessor
Ручные getXxx(), setXxx() для всех полейJS-6.8@Getter/@Setter
@RequiredArgsConstructor забыли на DI-классеJS-6.1обязателен

Куда дальше

  • Java → раздел 6. Lombok — нормативные формулировки.
  • Именование — @Getter генерирует get* префикс.
  • Современные фичи Java — record вместо @Value/@Data.
  • Observability → logging — @Slf4j детали.
  • Auth → PII — @ToString риск утечки.
  • JOOQ → repository — generated POJOs с @Getter/@Setter.
  • DDD → aggregate — named-конструкторы вместо @Builder.