80 Spring-Themen

Spring Refactoring Workbench

Alle zugehörigen Inhalte befinden sich auf dieser einen großen Seite. Kapitel und Beispiele sind standardmäßig geschlossen und lassen sich gezielt öffnen.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Lernzielorientierter Zugang

Spring nach Architekturgrenzen statt Annotationen lernen

Die vorhandenen Kapitel bleiben vollständig erhalten. Diese Orientierung gruppiert sie nach den Entscheidungen, die ein Enterprise-Team tatsächlich treffen muss.

Abhängigkeiten

Constructor Injection und explizite Konfiguration als Voraussetzung für testbare Komponenten.

Use Cases

Controller dünn halten und Transaktionsgrenzen in der Anwendungsschicht sichtbar machen.

Persistenz

Spring Data als Adapter behandeln; Domänenmodell und Repository-Vertrag schützen.

Integration

Events, Retry und externe Clients mit Idempotenz, Timeout und fachlicher Fehlerübersetzung verbinden.

Kapitel 1 Abhängigkeiten und Anwendungsschichten8 Fachthemen

Acht zentrale Refactorings, die aus schwer testbarem Spring-Code klar abgegrenzte Application-, Domain- und Adapter-Bausteine machen.

Spring-Themen · Abschnitt 1

Abhängigkeiten und Anwendungsschichten

Acht zentrale Refactorings, die aus schwer testbarem Spring-Code klar abgegrenzte Application-, Domain- und Adapter-Bausteine machen.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 1 Constructor Injection statt Field Injection Versteckte Abhängigkeiten werden zu einem expliziten, unveränderlichen Vertrag.

Constructor Injection statt Field Injection

Code Smell

Field Injection erschwert Unit-Tests und erlaubt unvollständig initialisierte Objekte.

Pattern / Prinzip

Constructor Injection + Dependency Inversion

Refactoring-Pfad

Abhängigkeiten im Konstruktor sichtbar machen, final speichern und mit Fake-Port testen.

Alternative

Field Injection nur in sehr kleinen Prototypen

Vorher

Spring-Ausgangscode
@Service
class RegistrationService {
  @Autowired private MailSender mailSender;
}

Nachher

Spring-Zielcode
@Service
final class RegistrationService {
  private final MailSender mailSender;
  RegistrationService(MailSender mailSender) {
    this.mailSender = mailSender;
  }
}

Kompilierbare Referenz

ConstructorInjectionRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Objects;
// Design Pattern: Dependency Injection
// Zweck: Abhängigkeiten explizit, unveränderlich und testbar machen.
public final class ConstructorInjectionRefactoring {
  public interface MailPort { void send(String address, String text); }
  public static final class RegistrationService {
    private final MailPort mailPort;
    public RegistrationService(MailPort mailPort) { this.mailPort = Objects.requireNonNull(mailPort); }
    public void register(String address) { mailPort.send(address, "Willkommen"); }
  }
}

Bereich 2 Typisierte Configuration Properties Zusammengehörige Einstellungen werden validiert und als fachlicher Konfigurationsbaustein injiziert.

Typisierte Configuration Properties

Code Smell

Viele @Value-Felder verteilen Namen, Defaults und Konvertierung über die Anwendung.

Pattern / Prinzip

Configuration Object

Refactoring-Pfad

Properties gruppieren, validieren, immutable übergeben und Konfigurationsfehler früh melden.

Alternative

Einzelnes @Value für wirklich isolierte Werte

Vorher

Spring-Ausgangscode
@Value("${payment.url}") String url;
@Value("${payment.timeout:2s}") Duration timeout;
@Value("${payment.retries:3}") int retries;

Nachher

Spring-Zielcode
@ConfigurationProperties("payment")
@Validated
record PaymentProperties(
  @NotBlank String url,
  @NotNull @Positive Duration timeout,
  @Min(0) int retries) {}

Kompilierbare Referenz

TypedConfigurationRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration;
// Design Pattern: Configuration Object
// Zweck: Zusammengehörige Konfiguration typisiert und validierbar bündeln.
public final class TypedConfigurationRefactoring {
  public record PaymentClientProperties(String baseUrl, Duration timeout, int retries) {
    public PaymentClientProperties {
      if (baseUrl == null || baseUrl.isBlank()) throw new IllegalArgumentException("baseUrl");
      if (timeout == null || timeout.isNegative() || timeout.isZero()) throw new IllegalArgumentException("timeout");
      if (retries < 0) throw new IllegalArgumentException("retries");
    }
  }
}

Bereich 3 Thin Controller und Application Use Case HTTP-Übersetzung bleibt im Adapter; Geschäftsablauf liegt in einem klaren Use Case.

Thin Controller und Application Use Case

Code Smell

Controller mit Validierung, Preisberechnung, Persistenz und Benachrichtigung wird zum God Object.

Pattern / Prinzip

Hexagonal Architecture + Application Service

Refactoring-Pfad

Request mappen, Use Case aufrufen, Ergebnis in HTTP-Antwort übersetzen.

Alternative

Einfaches CRUD ohne Fachlogik

Vorher

Spring-Ausgangscode
@PostMapping("/orders")
ResponseEntity<?> create(@RequestBody OrderRequest request) {
  validate(request); calculate(request); repository.save(...); mail(...);
  return ResponseEntity.ok(...);
}

Nachher

Spring-Zielcode
@PostMapping("/orders")
ResponseEntity<OrderResponse> create(@Valid @RequestBody OrderRequest request) {
  var result = useCase.execute(mapper.toCommand(request));
  return ResponseEntity.status(CREATED).body(mapper.toResponse(result));
}

Kompilierbare Referenz

ThinControllerRefactoring.java
package com.aydinsude.workbench.spring;
// Architecture Pattern: Ports and Adapters
// Zweck: Transportlogik vom fachlichen Anwendungsfall trennen.
public final class ThinControllerRefactoring {
  public record CreateOrderCommand(String customerId, long totalMinor) {}
  public record OrderResult(String orderId) {}
  public interface CreateOrderUseCase { OrderResult execute(CreateOrderCommand command); }
  public static final class OrderHttpAdapter {
    private final CreateOrderUseCase useCase;
    public OrderHttpAdapter(CreateOrderUseCase useCase) { this.useCase = useCase; }
    public OrderResult post(String customerId, long totalMinor) {
      return useCase.execute(new CreateOrderCommand(customerId, totalMinor));
    }
  }
}

Bereich 4 Zentrale Exception Translation Fachfehler werden an einer Stelle konsistent in HTTP-Problemantworten übersetzt.

Zentrale Exception Translation

Code Smell

Lokale try/catch-Blöcke duplizieren Statuscodes, Texte und Logging.

Pattern / Prinzip

Exception Translator

Refactoring-Pfad

Fachliche Exceptions stabilisieren und mit Controller Advice zentral übersetzen.

Alternative

Direkte lokale Behandlung bei transportnahen Sonderfällen

Vorher

Spring-Ausgangscode
try { return service.load(id); }
catch (Exception e) {
  return ResponseEntity.status(500).body(e.getMessage());
}

Nachher

Spring-Zielcode
@RestControllerAdvice
class ApiExceptionHandler {
  @ExceptionHandler(CustomerNotFound.class)
  ResponseEntity<ProblemDetail> notFound(CustomerNotFound e) {
    return ResponseEntity.of(ProblemDetail.forStatusAndDetail(NOT_FOUND, e.getMessage()));
  }
}

Kompilierbare Referenz

ExceptionTranslationRefactoring.java
package com.aydinsude.workbench.spring;
// Design Pattern: Exception Translator
// Zweck: Fachfehler von Transportfehlern entkoppeln.
public final class ExceptionTranslationRefactoring {
  public static final class CustomerNotFound extends RuntimeException {
    private final String customerId;
    public CustomerNotFound(String customerId) { super("Customer not found: " + customerId); this.customerId = customerId; }
    public String customerId() { return customerId; }
  }
  public record Problem(String code, String detail) {}
  public static Problem translate(CustomerNotFound error) {
    return new Problem("CUSTOMER_NOT_FOUND", error.getMessage());
  }
}

Bereich 5 Transaktionsgrenze im Application Service Eine fachliche Änderung wird innerhalb einer bewusst kleinen Transaktion atomar ausgeführt.

Transaktionsgrenze im Application Service

Code Smell

@Transactional auf Controller oder privaten Hilfsmethoden macht Grenzen unsichtbar und hält Transaktionen zu lange offen.

Pattern / Prinzip

Unit of Work + Application Service

Refactoring-Pfad

Datenbankarbeit bündeln; Remote-I/O vor oder nach der Transaktion ausführen.

Alternative

Programmatic TransactionTemplate bei dynamischen Grenzen

Vorher

Spring-Ausgangscode
@Transactional
@PostMapping("/orders/{id}/approve")
void approve(@PathVariable String id) {
  remoteRiskCheck(id); repository.approve(id); mail.send(id);
}

Nachher

Spring-Zielcode
@Service
class ApproveOrderUseCase {
  @Transactional
  void execute(OrderId id) {
    var order = repository.get(id);
    repository.save(order.approve());
  }
}

Kompilierbare Referenz

TransactionBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
// Design Pattern: Unit of Work
// Zweck: Eine fachliche Zustandsänderung atomar abgrenzen.
public final class TransactionBoundaryRefactoring {
  public interface OrderRepository { Order load(String id); void save(Order order); }
  public record Order(String id, String status) { Order approve() { return new Order(id, "APPROVED"); } }
  public static final class ApproveOrderUseCase {
    private final OrderRepository repository;
    public ApproveOrderUseCase(OrderRepository repository) { this.repository = repository; }
    public void execute(String id) {
      var order = repository.load(id);
      repository.save(order.approve());
    }
  }
}

Bereich 6 Repository Port statt JPA-Leak Domänen- und Anwendungslogik sprechen mit einem fachlichen Port statt mit EntityManager oder Spring Data Details.

Repository Port statt JPA-Leak

Code Smell

JPA-Entities, Lazy-Proxies und Query-APIs dringen bis in Controller und Domain vor.

Pattern / Prinzip

Repository + Adapter

Refactoring-Pfad

Fachlichen Repository-Port definieren und JPA-Mapping in den Infrastrukturadapter verschieben.

Alternative

Direkter Spring-Data-Einsatz in reinem CRUD-Modul

Vorher

Spring-Ausgangscode
@Service
class CustomerService {
  @PersistenceContext EntityManager em;
  CustomerEntity rename(Long id, String name) { ... }
}

Nachher

Spring-Zielcode
interface CustomerRepository {
  Optional<Customer> find(CustomerId id);
  void save(Customer customer);
}
@Repository
class JpaCustomerRepositoryAdapter implements CustomerRepository { ... }

Kompilierbare Referenz

RepositoryPortRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Optional;
// Design Pattern: Repository
// Zweck: Persistenzdetails hinter einem fachlichen Vertrag kapseln.
public final class RepositoryPortRefactoring {
  public record CustomerId(String value) {}
  public record Customer(CustomerId id, String name) {}
  public interface CustomerRepository {
    Optional<Customer> find(CustomerId id);
    void save(Customer customer);
  }
  public static final class RenameCustomer {
    private final CustomerRepository repository;
    public RenameCustomer(CustomerRepository repository) { this.repository = repository; }
    public void execute(CustomerId id, String name) {
      var current = repository.find(id).orElseThrow();
      repository.save(new Customer(current.id(), name));
    }
  }
}

Bereich 7 Validation Boundary Syntaktische Request-Prüfung und fachliche Invarianten werden getrennt und am richtigen Ort ausgeführt.

Validation Boundary

Code Smell

Controller-If-Ketten vermischen Nullprüfung, Format und Geschäftsregeln.

Pattern / Prinzip

Boundary Validation + Value Object

Refactoring-Pfad

Bean Validation am DTO; Invarianten im Value Object oder Aggregate schützen.

Alternative

Nur Domänenvalidierung bei intern erzeugten Commands

Vorher

Spring-Ausgangscode
if (request.email() == null || !request.email().contains("@")) ...
if (request.name() == null || request.name().isBlank()) ...
if (blockedDomains.contains(domain(request.email()))) ...

Nachher

Spring-Zielcode
record RegisterCustomerRequest(
  @NotBlank @Email String email,
  @NotBlank @Size(max=120) String name) {}
// Fachregel bleibt im Use Case / Value Object.

Kompilierbare Referenz

ValidationBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Value Object
// Zweck: Fachliche Invarianten unabhängig vom Transport schützen.
public final class ValidationBoundaryRefactoring {
  public record EmailAddress(String value) {
    public EmailAddress {
      if (value == null || !value.matches("^[^@]+@[^@]+\\.[^@]+$"))
        throw new IllegalArgumentException("email");
    }
  }
  public record RegisterCustomerCommand(EmailAddress email, String displayName) {
    public RegisterCustomerCommand {
      if (displayName == null || displayName.isBlank()) throw new IllegalArgumentException("displayName");
    }
  }
}

Bereich 8 Application Events statt direkter Folgekette Nach erfolgreicher Fachaktion reagieren unabhängige Handler entkoppelt auf ein stabiles Ereignis.

Application Events statt direkter Folgekette

Code Smell

Use Case ruft Mail, Audit, CRM und Analytics direkt nacheinander auf.

Pattern / Prinzip

Observer / Application Event

Refactoring-Pfad

Ereignis nach erfolgreicher Zustandsänderung publizieren und Handler idempotent gestalten.

Alternative

Direkter synchroner Aufruf bei genau einer zwingenden Folgeaktion

Vorher

Spring-Ausgangscode
void approve(Order order) {
  repository.save(order.approve());
  mail.send(order); audit.write(order); crm.sync(order); metrics.count();
}

Nachher

Spring-Zielcode
publisher.publishEvent(new OrderApproved(order.id(), clock.instant()));

@TransactionalEventListener(phase = AFTER_COMMIT)
void sendConfirmation(OrderApproved event) { ... }

Kompilierbare Referenz

ApplicationEventsRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
// Design Pattern: Observer / Application Event
// Zweck: Unabhängige Folgeaktionen vom Kern-Use-Case entkoppeln.
public final class ApplicationEventsRefactoring {
  public record OrderApproved(String orderId, Instant occurredAt) {}
  public interface EventPublisher { void publish(Object event); }
  public static final class ApprovalService {
    private final EventPublisher publisher;
    public ApprovalService(EventPublisher publisher) { this.publisher = publisher; }
    public void approve(String orderId) {
      // aggregate and repository work omitted
      publisher.publish(new OrderApproved(orderId, Instant.now()));
    }
  }
}
Kapitel 2 Web, Security und transaktionale Grenzen8 Fachthemen

HTTP-Verträge, Security, Query-Design, transaktionale Events, Caching und clusterfähige Jobs als klare, testbare Grenzen.

Spring-Themen · Abschnitt 2

Web, Security und transaktionale Grenzen

HTTP-Verträge, Security, Query-Design, transaktionale Events, Caching und clusterfähige Jobs als klare, testbare Grenzen.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 9 Explizites MVC Mapping Request- und Response-Modelle werden von Domain und Persistenz getrennt.

Explizites MVC Mapping

Code Smell

Controller gibt JPA-Entities direkt zurück und bindet API, Persistenz und Fachmodell zusammen.

Pattern / Prinzip

Mapper / Anti-Corruption Boundary

Refactoring-Pfad

Transport-DTO definieren, Mapping isolieren, Domainmodell unabhängig halten.

Alternative

Direkte Entity-Ausgabe nur bei internem Prototyp ohne stabilen Vertrag

Vorher

Spring-Ausgangscode
@GetMapping("/{id}")
OrderEntity get(@PathVariable long id) {
  return repository.findById(id).orElseThrow();
}

Nachher

Spring-Zielcode
@GetMapping("/{id}")
OrderResponse get(@PathVariable OrderId id) {
  return mapper.toResponse(query.load(id));
}

Kompilierbare Referenz

MvcMappingRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Mapper / Anti-Corruption Boundary
// Zweck: HTTP-Vertrag vom Fachmodell entkoppeln.
public final class MvcMappingRefactoring {
  public record Order(String id, long totalMinor, String status) {}
  public record OrderResponse(String id, String total, String status) {}
  public static final class OrderResponseMapper {
    public OrderResponse toResponse(Order order) {
      return new OrderResponse(order.id(), "EUR " + (order.totalMinor()/100.0), order.status());
    }
  }
}

Bereich 10 RFC-7807 ProblemDetail Fehlerantworten werden konsistent, maschinenlesbar und stabil versionierbar.

RFC-7807 ProblemDetail

Code Smell

Jeder Controller erzeugt eigene Maps oder Textmeldungen mit wechselnden Feldern.

Pattern / Prinzip

Problem Details / Exception Translator

Refactoring-Pfad

Fehlercode, Status, Detail und Korrelations-ID zentral abbilden.

Alternative

Kleine interne API mit exakt einem Fehlerformat

Vorher

Spring-Ausgangscode
catch (Exception e) {
  return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}

Nachher

Spring-Zielcode
@ExceptionHandler(OrderStateConflict.class)
ProblemDetail conflict(OrderStateConflict e) {
  var p = ProblemDetail.forStatusAndDetail(CONFLICT, e.getMessage());
  p.setProperty("code", "ORDER_STATE_CONFLICT");
  return p;
}

Kompilierbare Referenz

ProblemDetailRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Exception Translator
// Zweck: Stabile Fehlerverträge statt ad-hoc Maps.
public final class ProblemDetailRefactoring {
  public record ApiProblem(int status,String title,String detail,String code,Map<String,Object> properties) {}
  public static ApiProblem conflict(String detail,String traceId) {
    return new ApiProblem(409,"Conflict",detail,"ORDER_STATE_CONFLICT",Map.of("traceId",traceId));
  }
}

Bereich 11 Security Boundary am Adapter Authentifizierung wird in einen kleinen fachlichen Principal übersetzt; Domain bleibt frameworkfrei.

Security Boundary am Adapter

Code Smell

SecurityContext oder JWT-Claims werden tief in Services und Domain weitergereicht.

Pattern / Prinzip

Security Adapter / Principal Mapper

Refactoring-Pfad

Framework-Principal am Rand lesen und in ActorContext übersetzen.

Alternative

Direkte SecurityContext-Nutzung ausschließlich im Webadapter

Vorher

Spring-Ausgangscode
void approve(String id) {
  Authentication a = SecurityContextHolder.getContext().getAuthentication();
  service.approve(id, a);
}

Nachher

Spring-Zielcode
void approve(String id, Jwt jwt) {
  var actor = principalMapper.toActor(jwt);
  useCase.execute(id, actor);
}

Kompilierbare Referenz

SecurityBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set;
// Architecture Pattern: Security Adapter
// Zweck: Spring-Security-Typen an der Systemgrenze halten.
public final class SecurityBoundaryRefactoring {
  public record ActorContext(String subject, Set<String> roles) {
    public boolean hasRole(String role){ return roles.contains(role); }
  }
  public interface ApproveOrder { void execute(String orderId, ActorContext actor); }
}

Bereich 12 Method Security als fachliche Policy Autorisierung wird nicht über verstreute Rollenstrings, sondern über eine testbare Policy entschieden.

Method Security als fachliche Policy

Code Smell

Viele @PreAuthorize-Ausdrücke duplizieren Geschäftslogik und sind schwer isoliert testbar.

Pattern / Prinzip

Policy / Specification

Refactoring-Pfad

Security-Annotation auf grobe Grenze reduzieren und fachliche Entscheidung in Policy verschieben.

Alternative

Statische Rollenprüfung bei rein technischem Admin-Endpunkt

Vorher

Spring-Ausgangscode
@PreAuthorize("hasRole('SUPERVISOR') or (#order.amount < 1000 and #order.owner != authentication.name)")
void approve(Order order) { ... }

Nachher

Spring-Zielcode
@PreAuthorize("isAuthenticated()")
void approve(OrderId id) {
  useCase.execute(id, actorProvider.current());
}
// Use Case fragt ApprovalPolicy.

Kompilierbare Referenz

MethodSecurityPolicyRefactoring.java
package com.aydinsude.workbench.spring;
// Design Pattern: Policy
// Zweck: Fachliche Berechtigung explizit und testbar machen.
public final class MethodSecurityPolicyRefactoring {
  public record ApprovalContext(String ownerId,long amountMinor,String actorId,boolean supervisor) {}
  public static final class ApprovalPolicy {
    public boolean mayApprove(ApprovalContext c) {
      if (c.ownerId().equals(c.actorId())) return false;
      return c.amountMinor() <= 100_000 || c.supervisor();
    }
  }
}

Bereich 13 Spring-Data-Query als Query Port Lesefälle erhalten gezielte Projektionen statt Aggregate und Lazy-Graphen zu laden.

Spring-Data-Query als Query Port

Code Smell

findAll plus Filterung im Speicher, N+1-Zugriffe und Entity-Leaks in Reports.

Pattern / Prinzip

Query Object / CQRS Read Model

Refactoring-Pfad

Query-Port pro Lesefall definieren und Projektion gezielt befüllen.

Alternative

Einfaches Repository bei kleinen CRUD-Datenmengen

Vorher

Spring-Ausgangscode
var orders = orderRepository.findAll();
return orders.stream().filter(o -> inRange(o, from, to))
  .map(this::toRow).toList();

Nachher

Spring-Zielcode
interface RevenueProjection { LocalDate getDay(); long getRevenueMinor(); }
@Query("select ... group by ...")
List<RevenueProjection> revenue(LocalDate from, LocalDate to);

Kompilierbare Referenz

SpringDataQueryRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.LocalDate; import java.util.List;
// Pattern: Query Object / Read Model
// Zweck: Lesefall-spezifische Projektion statt Aggregate-Overfetching.
public final class SpringDataQueryRefactoring {
  public record RevenueRow(LocalDate day,long revenueMinor) {}
  public interface RevenueQuery { List<RevenueRow> between(LocalDate from, LocalDate to); }
  public static long total(RevenueQuery q,LocalDate f,LocalDate t){
    return q.between(f,t).stream().mapToLong(RevenueRow::revenueMinor).sum();
  }
}

Bereich 14 Transactional Event nach Commit Folgeaktionen werden erst nach erfolgreichem Commit gestartet und erhalten einen klaren Fehlerpfad.

Transactional Event nach Commit

Code Smell

Event-Listener läuft innerhalb der Transaktion und verschickt E-Mail, obwohl später rollback erfolgt.

Pattern / Prinzip

Transactional Observer / Outbox Boundary

Refactoring-Pfad

After-Commit-Listener oder Outbox verwenden; Handler idempotent gestalten.

Alternative

Synchroner Listener für reine In-Memory-Aktualisierung

Vorher

Spring-Ausgangscode
@EventListener
void on(InvoiceIssued e) { mail.send(e.invoiceId()); }
// läuft möglicherweise vor Commit-Abschluss

Nachher

Spring-Zielcode
@TransactionalEventListener(phase = AFTER_COMMIT)
void on(InvoiceIssued e) { mail.send(e.invoiceId()); }
// Für garantierte Zustellung: Outbox.

Kompilierbare Referenz

TransactionalEventRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
// Pattern: Transactional Observer
// Zweck: Seiteneffekt erst nach bestätigter Zustandsänderung.
public final class TransactionalEventRefactoring {
  public record InvoiceIssued(String invoiceId, Instant occurredAt) {}
  public interface AfterCommitPublisher { void publish(Object event); }
  public static final class IssueInvoice {
    private final AfterCommitPublisher publisher;
    public IssueInvoice(AfterCommitPublisher publisher){this.publisher=publisher;}
    public void execute(String id){ publisher.publish(new InvoiceIssued(id,Instant.now())); }
  }
}

Bereich 15 Cache Abstraction mit klarer Semantik Cache-Key, TTL, Null- und Invalidierungsregeln werden Teil eines expliziten Lesevertrags.

Cache Abstraction mit klarer Semantik

Code Smell

@Cacheable wird wahllos auf Services verteilt; Keys kollidieren und Änderungen invalidieren nicht.

Pattern / Prinzip

Cache-Aside / Read-Through

Refactoring-Pfad

Cache nur am stabilen Query-Port, Version im Key und gezielte Eviction nach Mutation.

Alternative

Kein Cache ohne gemessenen Engpass

Vorher

Spring-Ausgangscode
@Cacheable("products")
Product load(String id) { return repository.find(id); }

Nachher

Spring-Zielcode
@Cacheable(cacheNames="products-v2", key="#tenant + ':' + #id", unless="#result == null")
ProductView load(TenantId tenant, ProductId id) { ... }
@CacheEvict(cacheNames="products-v2", key="#tenant + ':' + #id")

Kompilierbare Referenz

CacheAbstractionRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
// Pattern: Cache-Aside
// Zweck: Cache-Semantik explizit und testbar halten.
public final class CacheAbstractionRefactoring {
  public interface ProductSource { String load(String tenant,String id); }
  public static final class CachedProductQuery {
    private final ProductSource source; private final Map<String,String> cache=new ConcurrentHashMap<>();
    public CachedProductQuery(ProductSource source){this.source=source;}
    public String get(String tenant,String id){ return cache.computeIfAbsent("v2:"+tenant+":"+id,k->source.load(tenant,id)); }
    public void evict(String tenant,String id){ cache.remove("v2:"+tenant+":"+id); }
  }
}

Bereich 16 Scheduling mit verteiltem Lock Geplante Jobs werden idempotent und clusterfähig; parallele Instanzen führen denselben Lauf nicht doppelt aus.

Scheduling mit verteiltem Lock

Code Smell

@Scheduled-Methode verarbeitet alles ohne Run-ID, Lock oder Wiederanlaufstrategie.

Pattern / Prinzip

Scheduler Lock + Idempotent Command

Refactoring-Pfad

Job als Command modellieren, Run-ID persistieren, Lock klein halten und Arbeit checkpointen.

Alternative

Einzelinstanziger lokaler Wartungsjob ohne Seiteneffekt

Vorher

Spring-Ausgangscode
@Scheduled(cron="0 */5 * * * *")
void reconcile() { repository.findAllOpen().forEach(this::process); }

Nachher

Spring-Zielcode
@Scheduled(cron="${jobs.reconcile.cron}")
@SchedulerLock(name="reconcile", lockAtMostFor="PT5M")
void reconcile() { command.execute(runIdFactory.next()); }

Kompilierbare Referenz

SchedulingLockRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
// Pattern: Distributed Lock + Idempotent Command
// Zweck: Clusterweiten Job genau kontrolliert starten.
public final class SchedulingLockRefactoring {
  public interface JobLock { boolean tryAcquire(String name,Instant until); void release(String name); }
  public interface Reconciliation { void run(String runId); }
  public static final class ScheduledReconciliation {
    private final JobLock lock; private final Reconciliation work;
    public ScheduledReconciliation(JobLock lock,Reconciliation work){this.lock=lock;this.work=work;}
    public void trigger(String runId){
      if(!lock.tryAcquire("reconciliation",Instant.now().plusSeconds(300))) return;
      try{ work.run(runId); } finally { lock.release("reconciliation"); }
    }
  }
}
Kapitel 3 Robuste HTTP- und Integrationsgrenzen8 Fachthemen

Robuste HTTP- und Integrationsgrenzen: Pagination, Nebenläufigkeit, Uploads, WebClient, Resilienz, Observability und Korrelationskontext.

Spring-Themen · Abschnitt 3

Robuste HTTP- und Integrationsgrenzen

Robuste HTTP- und Integrationsgrenzen: Pagination, Nebenläufigkeit, Uploads, WebClient, Resilienz, Observability und Korrelationskontext.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 17 REST Pagination mit stabilem Cursor Große Ergebnislisten werden stabil, begrenzt und ohne Offset-Drift ausgeliefert.

REST Pagination mit stabilem Cursor

Code Smell

Controller lädt komplette Listen oder verwendet Offset-Pagination bei gleichzeitigem Schreiben.

Pattern / Prinzip

Cursor Pagination / Query Object

Refactoring-Pfad

Sortierschlüssel definieren, Cursor signieren und Query-Port auf Limit plus Cursor ausrichten.

Alternative

Offset-Pagination bei kleinen, unveränderlichen Datenbeständen

Vorher

Spring-Ausgangscode
@GetMapping
List<OrderEntity> all() { return repository.findAll(); }

Nachher

Spring-Zielcode
@GetMapping
OrderPageResponse page(@RequestParam(required=false) String cursor,
                       @RequestParam(defaultValue="50") int limit) {
  return mapper.toResponse(query.loadAfter(cursorCodec.decode(cursor), limit));
}

Kompilierbare Referenz

RestPaginationRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Cursor Pagination + Query Object
// Zweck: Stabile Seiten trotz paralleler Änderungen liefern.
public final class RestPaginationRefactoring {
  public record Cursor(long createdAtEpochMilli,String id) {}
  public record Page<T>(List<T> items, Cursor nextCursor, boolean hasMore) {}
  public record OrderView(String id,long createdAtEpochMilli) {}
  public interface OrderPageQuery { Page<OrderView> loadAfter(Cursor cursor,int limit); }
}

Bereich 18 ETag und optimistische Nebenläufigkeit HTTP-Änderungen erkennen verlorene Updates und antworten mit klaren Precondition-Fehlern.

ETag und optimistische Nebenläufigkeit

Code Smell

PUT überschreibt Daten ohne Versionsprüfung; parallele Änderungen gehen unbemerkt verloren.

Pattern / Prinzip

Optimistic Lock / Conditional Request

Refactoring-Pfad

Version als ETag ausgeben, If-Match verlangen und Konflikt als 412 oder 409 übersetzen.

Alternative

Pessimistisches Locking bei kurzer, hochkritischer Transaktion

Vorher

Spring-Ausgangscode
@PutMapping("/{id}")
OrderResponse replace(@PathVariable String id,@RequestBody UpdateOrder body) {
  return service.replace(id, body);
}

Nachher

Spring-Zielcode
@PutMapping("/{id}")
ResponseEntity<OrderResponse> replace(@PathVariable String id,
  @RequestHeader("If-Match") String etag,@RequestBody UpdateOrder body) {
  var result = useCase.replace(id, etag, body);
  return ResponseEntity.ok().eTag(result.etag()).body(mapper.toResponse(result));
}

Kompilierbare Referenz

EtagConcurrencyRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Optimistic Lock + Conditional Request
// Zweck: Lost Updates an der HTTP-Grenze verhindern.
public final class EtagConcurrencyRefactoring {
  public record VersionedOrder(String id,long version,String status) {
    public String etag(){ return "\"order-"+id+"-v"+version+"\""; }
  }
  public static void requireMatch(String ifMatch,VersionedOrder order){
    if(ifMatch==null || !ifMatch.equals(order.etag())) throw new PreconditionFailed(order.etag());
  }
  public static final class PreconditionFailed extends RuntimeException {
    private final String currentEtag;
    public PreconditionFailed(String currentEtag){super("stale representation");this.currentEtag=currentEtag;}
    public String currentEtag(){return currentEtag;}
  }
}

Bereich 19 Multipart- und Datei-Grenze Dateiuploads werden gestreamt, validiert und über einen Port verarbeitet statt im Controller gepuffert.

Multipart- und Datei-Grenze

Code Smell

MultipartFile wird vollständig als byte[] geladen und direkt in Domain oder Datenbank weitergereicht.

Pattern / Prinzip

Streaming Port / Boundary Object

Refactoring-Pfad

Metadaten und Stream trennen, Größe und Typ am Rand prüfen, Storage-Port verwenden.

Alternative

Kleine Konfigurationsdateien mit strengem Größenlimit

Vorher

Spring-Ausgangscode
byte[] bytes = file.getBytes();
return documentService.store(file.getOriginalFilename(), bytes);

Nachher

Spring-Zielcode
var metadata = mapper.toMetadata(file);
UploadPolicy.validate(metadata);
return ingest.ingest(metadata, file::getInputStream);

Kompilierbare Referenz

MultipartBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.io.IOException; import java.io.InputStream;
// Pattern: Streaming Port + Boundary Object
// Zweck: Große Uploads ohne Speicherexplosion verarbeiten.
public final class MultipartBoundaryRefactoring {
  public record UploadMetadata(String fileName,String contentType,long contentLength) {}
  public interface ContentSource { InputStream open() throws IOException; }
  public interface DocumentIngest { String ingest(UploadMetadata metadata,ContentSource source) throws IOException; }
  public static void validate(UploadMetadata m,long maxBytes){
    if(m.contentLength()<0 || m.contentLength()>maxBytes) throw new IllegalArgumentException("invalid size");
    if(!m.contentType().equals("application/pdf")) throw new IllegalArgumentException("unsupported type");
  }
}

Bereich 20 WebClient als ausgehender Adapter Reaktive HTTP-Details bleiben im Infrastrukturadapter; der Use Case arbeitet gegen einen fachlichen Port.

WebClient als ausgehender Adapter

Code Smell

WebClient, URI-Bau, Header und Statuscodes sind direkt im Application Service verteilt.

Pattern / Prinzip

Outbound Adapter / Anti-Corruption Layer

Refactoring-Pfad

Fachlichen Port definieren, DTO-Mapping und Fehlerübersetzung im Adapter kapseln.

Alternative

Direkter Client nur in sehr kleinem technischen Integrationsdienst

Vorher

Spring-Ausgangscode
var response = webClient.get().uri("/risk/{id}", customerId)
  .retrieve().bodyToMono(RemoteRiskResponse.class).block();
return approve(response.score());

Nachher

Spring-Zielcode
RiskScore score = riskAssessment.assess(customerId);
return approvalPolicy.decide(score);

Kompilierbare Referenz

WebClientAdapterRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Outbound Adapter + Anti-Corruption Layer
// Zweck: Remote-API und Spring-WebClient vom Use Case entkoppeln.
public final class WebClientAdapterRefactoring {
  public record RiskScore(int value,String source) {}
  public interface RiskAssessmentPort { RiskScore assess(String customerId); }
  public record RemoteRiskResponse(int score,String provider) {}
  public static RiskScore map(RemoteRiskResponse response){
    if(response.score()<0 || response.score()>100) throw new IllegalStateException("invalid remote score");
    return new RiskScore(response.score(),response.provider());
  }
}

Bereich 21 Retry und Timeout nach Fehlerklasse Wiederholungen sind begrenzt, messbar und nur für transient sichere Operationen aktiviert.

Retry und Timeout nach Fehlerklasse

Code Smell

Jeder Fehler wird mehrfach wiederholt; fehlende Timeouts blockieren Threads und verstärken Last.

Pattern / Prinzip

Retry Policy / Timeout / Idempotency

Refactoring-Pfad

Fehler klassifizieren, Deadline setzen, Backoff begrenzen und Idempotency-Key mitgeben.

Alternative

Kein Retry bei fachlichen 4xx-Fehlern oder nicht-idempotenten Aufrufen

Vorher

Spring-Ausgangscode
return retryTemplate.execute(ctx -> webClient.post().retrieve().bodyToMono(Result.class).block());

Nachher

Spring-Zielcode
return resilientClient.call(
  Request.withIdempotencyKey(command.id()),
  TimeoutBudget.ofSeconds(2),
  RetryPolicy.transientFailures(3));

Kompilierbare Referenz

RetryTimeoutRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration; import java.util.function.Supplier;
// Pattern: Retry Policy + Timeout Budget
// Zweck: Nur transiente und sichere Aufrufe kontrolliert wiederholen.
public final class RetryTimeoutRefactoring {
  public interface Sleeper { void sleep(Duration duration) throws InterruptedException; }
  public static <T> T execute(Supplier<T> call,int maxAttempts,Duration backoff){
    RuntimeException last=null;
    for(int attempt=1;attempt<=maxAttempts;attempt++){
      try{return call.get();}catch(TransientRemoteFailure ex){last=ex;if(attempt==maxAttempts)break;}
    }
    throw last;
  }
  public static final class TransientRemoteFailure extends RuntimeException { public TransientRemoteFailure(String m){super(m);} }
}

Bereich 22 Circuit Breaker an der Integrationsgrenze Ein instabiler Downstream wird schnell isoliert; Fallback und Recovery sind explizit.

Circuit Breaker an der Integrationsgrenze

Code Smell

Jeder Request wartet erneut auf denselben ausgefallenen Dienst und erschöpft Thread- und Connection-Pools.

Pattern / Prinzip

Circuit Breaker / Fallback

Refactoring-Pfad

Breaker nur im Adapter, Zustände messen, fachlich sinnvollen Fallback definieren.

Alternative

Retry ohne Circuit Breaker bei sehr seltenen, kurzen Netzwerkfehlern

Vorher

Spring-Ausgangscode
return remoteCatalog.load(productId);

Nachher

Spring-Zielcode
return catalogBreaker.execute(
  () -> remoteCatalog.load(productId),
  () -> localSnapshot.find(productId));

Kompilierbare Referenz

CircuitBreakerRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant; import java.util.function.Supplier;
// Pattern: Circuit Breaker
// Zweck: Kaskadierende Ausfälle an der Remote-Grenze begrenzen.
public final class CircuitBreakerRefactoring {
  enum State { CLOSED, OPEN, HALF_OPEN }
  public static final class Breaker {
    private State state=State.CLOSED; private int failures; private Instant openedAt;
    public <T> T call(Supplier<T> primary,Supplier<T> fallback){
      if(state==State.OPEN) return fallback.get();
      try { T value=primary.get(); failures=0; state=State.CLOSED; return value; }
      catch(RuntimeException ex){ if(++failures>=3){state=State.OPEN;openedAt=Instant.now();} return fallback.get(); }
    }
    public State state(){return state;}
  }
}

Bereich 23 Observability mit Micrometer-Grenze Metriken beschreiben fachliche Ergebnisse, ohne technische Instrumentierung in Domain-Code zu verteilen.

Observability mit Micrometer-Grenze

Code Smell

Counter und Timer werden überall manuell aufgerufen; Tags haben unkontrollierte Kardinalität.

Pattern / Prinzip

Decorator / Observability Port

Refactoring-Pfad

Use Case dekorieren, feste Low-Cardinality-Tags verwenden und Trace-IDs nicht als Metriktag nutzen.

Alternative

Direkte technische Metrik im reinen Infrastrukturadapter

Vorher

Spring-Ausgangscode
registry.counter("orders", "customer", customerId).increment();
return service.place(command);

Nachher

Spring-Zielcode
var observed = new ObservedCommand<>("order.place", useCase, metrics);
return observed.execute(command);

Kompilierbare Referenz

ObservabilityRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration; import java.time.Instant;
// Pattern: Decorator + Observability Port
// Zweck: Fachliche Metriken ohne Framework-Leak erfassen.
public final class ObservabilityRefactoring {
  public interface Metrics { void success(String operation,Duration duration); void failure(String operation,String category,Duration duration); }
  public interface Command<T,R> { R execute(T input); }
  public static final class ObservedCommand<T,R> implements Command<T,R> {
    private final String name; private final Command<T,R> delegate; private final Metrics metrics;
    public ObservedCommand(String name,Command<T,R> delegate,Metrics metrics){this.name=name;this.delegate=delegate;this.metrics=metrics;}
    public R execute(T input){var start=Instant.now();try{var r=delegate.execute(input);metrics.success(name,Duration.between(start,Instant.now()));return r;}catch(RuntimeException ex){metrics.failure(name,ex.getClass().getSimpleName(),Duration.between(start,Instant.now()));throw ex;}}
  }
}

Bereich 24 Correlation Context ohne ThreadLocal-Leak Korrelationsdaten werden explizit über Grenzen transportiert und bei asynchroner Arbeit sicher propagiert.

Correlation Context ohne ThreadLocal-Leak

Code Smell

Statisches ThreadLocal verliert Kontext bei Executor- oder Reactive-Wechseln und kann Daten zwischen Requests leaken.

Pattern / Prinzip

Context Object / Context Propagation

Refactoring-Pfad

CorrelationContext als Wertobjekt am Rand erzeugen, explizit weitergeben und Adapter für Logs/Headers verwenden.

Alternative

ThreadLocal ausschließlich in kleinem synchronen Servlet-Adapter mit garantiertem Cleanup

Vorher

Spring-Ausgangscode
CorrelationHolder.set(request.getHeader("X-Correlation-ID"));
executor.submit(() -> service.run(command));

Nachher

Spring-Zielcode
var context = correlationMapper.from(requestHeaders);
executor.submit(() -> useCase.execute(command, context.child()));

Kompilierbare Referenz

CorrelationContextRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Objects; import java.util.UUID;
// Pattern: Context Object + Explicit Context Propagation
// Zweck: Korrelationsdaten thread- und frameworkunabhängig transportieren.
public final class CorrelationContextRefactoring {
  public record CorrelationContext(String correlationId,String causationId) {
    public CorrelationContext { Objects.requireNonNull(correlationId); }
    public static CorrelationContext root(){return new CorrelationContext(UUID.randomUUID().toString(),null);}
    public CorrelationContext child(){return new CorrelationContext(correlationId,UUID.randomUUID().toString());}
  }
  public interface ContextualCommand<T,R> { R execute(T input,CorrelationContext context); }
}
Kapitel 4 Persistenz, Outbox und Messaging8 Fachthemen

Persistenz und Messaging sauber entkoppeln: Query-Spezifikationen, Fetch Plans, Batch Writes, Outbox, Kafka-Grenzen, Idempotenz, Dead Letters und Schema Evolution.

Spring-Themen · Abschnitt 4

Persistenz, Outbox und Messaging

Persistenz und Messaging sauber entkoppeln: Query-Spezifikationen, Fetch Plans, Batch Writes, Outbox, Kafka-Grenzen, Idempotenz, Dead Letters und Schema Evolution.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 25 Spring Data Specifications statt Repository-Methodenexplosion Filter als typisierte Kriterien modellieren, kombinierbare Spezifikationen erzeugen und das Repository auf eine Suchoperation reduzieren.

Spring Data Specifications statt Repository-Methodenexplosion

Code Smell

Viele abgeleitete Repository-Methoden kombinieren Filtervarianten unübersichtlich.

Pattern / Prinzip

Specification / Query Object

Refactoring-Pfad

Filter als typisierte Kriterien modellieren, kombinierbare Spezifikationen erzeugen und das Repository auf eine Suchoperation reduzieren.

Alternative

Dedizierte Query-Klasse bei sehr komplexen Reporting-Abfragen

Vorher

Spring-Ausgangscode
List<OrderEntity> findByStatusAndRegionAndPriority(String s,String r,int p);

Nachher

Spring-Zielcode
var criteria = new OrderCriteria(status, region, minimumPriority);
return orderQuery.search(criteria);

Kompilierbare Referenz

SpringDataSpecificationRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List; import java.util.Objects; import java.util.function.Predicate;
// Pattern: Specification + Query Object
// Zweck: Kombinierbare Suchregeln statt Repository-Methodenexplosion.
public final class SpringDataSpecificationRefactoring {
  public record Order(String id,String status,String region,int priority) {}
  public record OrderCriteria(String status,String region,int minimumPriority) {}
  public static Predicate<Order> specification(OrderCriteria c){
    Objects.requireNonNull(c);
    return o -> (c.status()==null || c.status().equals(o.status()))
      && (c.region()==null || c.region().equals(o.region()))
      && o.priority() >= c.minimumPriority();
  }
  public static List<Order> search(List<Order> source,OrderCriteria c){return source.stream().filter(specification(c)).toList();}
}

Bereich 26 JPA Fetch Plans gegen N+1 Use-Case-spezifische Projektion definieren, Fetch Plan explizit wählen und Aggregate-Grenzen nicht durch globale EAGER-Optionen verwischen.

JPA Fetch Plans gegen N+1

Code Smell

Lazy Beziehungen werden in Schleifen unkontrolliert nachgeladen.

Pattern / Prinzip

Fetch Plan / Data Mapper

Refactoring-Pfad

Use-Case-spezifische Projektion definieren, Fetch Plan explizit wählen und Aggregate-Grenzen nicht durch globale EAGER-Optionen verwischen.

Alternative

Batch Fetching bei kontrollierter Objektgraph-Nutzung

Vorher

Spring-Ausgangscode
orders.forEach(o -> response.add(new View(o.id(), o.customer().name())));

Nachher

Spring-Zielcode
return orderReadRepository.findSummaries(query);

Kompilierbare Referenz

JpaFetchPlanRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Fetch Plan + Projection
// Zweck: Benötigte Lesedaten in einem expliziten Query-Vertrag laden.
public final class JpaFetchPlanRefactoring {
  public record OrderSummary(String orderId,String customerName,long totalCents) {}
  public record OrderSummaryQuery(String region,int limit) {}
  public interface OrderSummaryRepository { List<OrderSummary> findSummaries(OrderSummaryQuery query); }
  public static final class UseCase {
    private final OrderSummaryRepository repository;
    public UseCase(OrderSummaryRepository repository){this.repository=repository;}
    public List<OrderSummary> execute(OrderSummaryQuery query){return repository.findSummaries(query);}
  }
}

Bereich 27 Batch Writes mit kontrolliertem Flush Batchgröße festlegen, persistieren, regelmäßig flushen und clear ausführen; fachliche Transaktionsgrenze beibehalten.

Batch Writes mit kontrolliertem Flush

Code Smell

Einzelne save-Aufrufe erzeugen viele Roundtrips und einen wachsenden Persistence Context.

Pattern / Prinzip

Unit of Work / Batch Writer

Refactoring-Pfad

Batchgröße festlegen, persistieren, regelmäßig flushen und clear ausführen; fachliche Transaktionsgrenze beibehalten.

Alternative

JDBC Batch Adapter bei reinem Import ohne Aggregate-Verhalten

Vorher

Spring-Ausgangscode
rows.forEach(repository::save);

Nachher

Spring-Zielcode
batchWriter.write(rows, 100);

Kompilierbare Referenz

BatchWriteRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Unit of Work + Batch Writer
// Zweck: Roundtrips und Speicherverbrauch bei Massenschreiben begrenzen.
public final class BatchWriteRefactoring {
  public interface PersistenceSession<T>{ void persist(T value); void flush(); void clear(); }
  public static final class BatchWriter<T>{
    private final PersistenceSession<T> session;
    public BatchWriter(PersistenceSession<T> session){this.session=session;}
    public void write(List<T> values,int batchSize){
      if(batchSize<1) throw new IllegalArgumentException("batchSize");
      for(int i=0;i<values.size();i++){session.persist(values.get(i)); if((i+1)%batchSize==0){session.flush();session.clear();}}
      session.flush();session.clear();
    }
  }
}

Bereich 28 Transactional Outbox mit Spring-Transaktion Fachänderung und Outbox-Eintrag in derselben Transaktion speichern; separater Publisher übernimmt Zustellung und Retry.

Transactional Outbox mit Spring-Transaktion

Code Smell

Datenbankänderung und Message-Publish bilden einen fehleranfälligen Dual Write.

Pattern / Prinzip

Transactional Outbox

Refactoring-Pfad

Fachänderung und Outbox-Eintrag in derselben Transaktion speichern; separater Publisher übernimmt Zustellung und Retry.

Alternative

CDC-basierte Outbox bei hoher Last

Vorher

Spring-Ausgangscode
repository.save(order);
kafkaTemplate.send("orders", event);

Nachher

Spring-Zielcode
transaction.execute(() -> { repository.save(order); outbox.append(event); });

Kompilierbare Referenz

TransactionalOutboxSpringRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant; import java.util.UUID;
// Pattern: Transactional Outbox
// Zweck: Fachänderung und Versandabsicht atomar speichern.
public final class TransactionalOutboxSpringRefactoring {
  public record OutboxMessage(UUID id,String topic,String key,String payload,Instant occurredAt) {}
  public interface OrderRepository { void save(String orderId,String status); }
  public interface Outbox { void append(OutboxMessage message); }
  public static final class CompleteOrder {
    private final OrderRepository orders; private final Outbox outbox;
    public CompleteOrder(OrderRepository orders,Outbox outbox){this.orders=orders;this.outbox=outbox;}
    public void execute(String id){orders.save(id,"COMPLETED");outbox.append(new OutboxMessage(UUID.randomUUID(),"orders.completed",id,"{\"orderId\":\""+id+"\"}",Instant.now()));}
  }
}

Bereich 29 Kafka Producer als ausgehender Port Fachlichen Event-Publisher definieren; Kafka-Adapter übernimmt Serialisierung, Topic-Routing und technische Header.

Kafka Producer als ausgehender Port

Code Smell

Domänen- oder Application-Code hängt direkt an KafkaTemplate, Topicnamen und Headerdetails.

Pattern / Prinzip

Port & Adapter / Message Envelope

Refactoring-Pfad

Fachlichen Event-Publisher definieren; Kafka-Adapter übernimmt Serialisierung, Topic-Routing und technische Header.

Alternative

Direkter KafkaTemplate-Aufruf im kleinen reinen Infrastrukturservice

Vorher

Spring-Ausgangscode
kafkaTemplate.send("order-events", orderId, json);

Nachher

Spring-Zielcode
domainEvents.publish(new OrderCompleted(orderId));

Kompilierbare Referenz

KafkaProducerBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant; import java.util.Map;
// Pattern: Outbound Port + Adapter
// Zweck: Kafka-spezifische Details aus Application- und Domain-Code entfernen.
public final class KafkaProducerBoundaryRefactoring {
  public sealed interface DomainEvent permits OrderCompleted { String aggregateId(); Instant occurredAt(); }
  public record OrderCompleted(String aggregateId,Instant occurredAt) implements DomainEvent {}
  public interface DomainEventPublisher { void publish(DomainEvent event); }
  public record MessageEnvelope(String topic,String key,byte[] payload,Map<String,String> headers) {}
  public interface MessageBroker { void send(MessageEnvelope envelope); }
  public static final class KafkaDomainEventAdapter implements DomainEventPublisher {
    private final MessageBroker broker;
    public KafkaDomainEventAdapter(MessageBroker broker){this.broker=broker;}
    public void publish(DomainEvent event){broker.send(new MessageEnvelope("order-events",event.aggregateId(),event.toString().getBytes(),Map.of("eventType",event.getClass().getSimpleName())));}
  }
}

Bereich 30 Kafka Consumer Idempotenz Message-ID vor Verarbeitung atomar reservieren; nur der Gewinner verarbeitet; Ergebnis und Status nachvollziehbar speichern.

Kafka Consumer Idempotenz

Code Smell

Wiederholte Zustellung führt zu doppelten Buchungen oder E-Mails.

Pattern / Prinzip

Idempotent Consumer / Inbox

Refactoring-Pfad

Message-ID vor Verarbeitung atomar reservieren; nur der Gewinner verarbeitet; Ergebnis und Status nachvollziehbar speichern.

Alternative

Natürlich idempotente Set-Operation ohne zusätzliche Inbox

Vorher

Spring-Ausgangscode
@KafkaListener void on(String payload) { bookingService.book(payload); }

Nachher

Spring-Zielcode
if (inbox.reserve(message.id())) handler.handle(message);

Kompilierbare Referenz

KafkaConsumerIdempotencyRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set; import java.util.concurrent.ConcurrentHashMap;
// Pattern: Idempotent Consumer + Inbox
// Zweck: At-least-once-Zustellung ohne doppelte Fachwirkung verarbeiten.
public final class KafkaConsumerIdempotencyRefactoring {
  public record IncomingMessage(String id,String payload) {}
  public interface Handler { void handle(String payload); }
  public static final class Inbox { private final Set<String> ids=ConcurrentHashMap.newKeySet(); public boolean reserve(String id){return ids.add(id);} }
  public static final class Consumer {
    private final Inbox inbox; private final Handler handler;
    public Consumer(Inbox inbox,Handler handler){this.inbox=inbox;this.handler=handler;}
    public boolean on(IncomingMessage message){if(!inbox.reserve(message.id())) return false;handler.handle(message.payload());return true;}
  }
}

Bereich 31 Dead Letter Handling mit Fehlerklassifikation Transient, permanent und poison message unterscheiden; Retry-Budget anwenden; DLQ-Nachricht mit Ursache und Originalmetadaten erzeugen.

Dead Letter Handling mit Fehlerklassifikation

Code Smell

Alle Consumer-Fehler werden gleich oft wiederholt oder kommentarlos verworfen.

Pattern / Prinzip

Dead Letter Channel / Error Classification

Refactoring-Pfad

Transient, permanent und poison message unterscheiden; Retry-Budget anwenden; DLQ-Nachricht mit Ursache und Originalmetadaten erzeugen.

Alternative

Sofortige fachliche Ablehnung bei synchron validierbaren Nachrichten

Vorher

Spring-Ausgangscode
catch (Exception e) { throw e; }

Nachher

Spring-Zielcode
return classifier.classify(error) == PERMANENT ? deadLetters.publish(failure) : RETRY;

Kompilierbare Referenz

DeadLetterHandlingRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Dead Letter Channel + Error Classification
// Zweck: Nicht zustellbare Nachrichten nachvollziehbar isolieren.
public final class DeadLetterHandlingRefactoring {
  public enum FailureKind { TRANSIENT, PERMANENT, POISON }
  public enum Action { RETRY, DEAD_LETTER }
  public record FailedMessage(String id,String payload,String reason,FailureKind kind) {}
  public interface Classifier { FailureKind classify(Throwable error); }
  public interface DeadLetters { void publish(FailedMessage failure); }
  public static Action decide(String id,String payload,Throwable error,Classifier classifier,DeadLetters dlq){
    var kind=classifier.classify(error); if(kind==FailureKind.TRANSIENT) return Action.RETRY;
    dlq.publish(new FailedMessage(id,payload,error.getMessage(),kind)); return Action.DEAD_LETTER;
  }
}

Bereich 32 Schema Evolution für Events Explizite Eventversion transportieren, alte Versionen am Consumer-Rand upcasten und additive Änderungen bevorzugen.

Schema Evolution für Events

Code Smell

Producer ändert Payloads ohne Versionierungs- oder Kompatibilitätsregel.

Pattern / Prinzip

Upcaster / Tolerant Reader

Refactoring-Pfad

Explizite Eventversion transportieren, alte Versionen am Consumer-Rand upcasten und additive Änderungen bevorzugen.

Alternative

Neues Topic bei semantisch inkompatiblem Ereignis

Vorher

Spring-Ausgangscode
record OrderEvent(String id,String customerName) {}

Nachher

Spring-Zielcode
var current = upcasters.toCurrent(envelope.version(), envelope.payload());

Kompilierbare Referenz

EventSchemaEvolutionRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Upcaster + Tolerant Reader
// Zweck: Alte Eventversionen kontrolliert in das aktuelle Modell überführen.
public final class EventSchemaEvolutionRefactoring {
  public record EventEnvelope(int version,Map<String,String> payload) {}
  public record CurrentOrderEvent(String orderId,String customerId,String currency) {}
  public interface Upcaster { CurrentOrderEvent toCurrent(EventEnvelope envelope); }
  public static final class OrderEventUpcaster implements Upcaster {
    public CurrentOrderEvent toCurrent(EventEnvelope e){
      if(e.version()==1) return new CurrentOrderEvent(e.payload().get("id"),e.payload().getOrDefault("customerId","unknown"),"EUR");
      if(e.version()==2) return new CurrentOrderEvent(e.payload().get("orderId"),e.payload().get("customerId"),e.payload().getOrDefault("currency","EUR"));
      throw new IllegalArgumentException("Unsupported event version: "+e.version());
    }
  }
}
Kapitel 5 Batch, Integration und Wiederanlauf8 Fachthemen

Spring Batch und Integration sauber schneiden: Job Boundary, Chunk-Transaktionen, Reader-Adapter, Fehlerpolitik, Restartability, Partitionierung, Integration Channels und AMQP Publisher Confirms.

Spring-Themen · Abschnitt 5

Batch, Integration und Wiederanlauf

Spring Batch und Integration sauber schneiden: Job Boundary, Chunk-Transaktionen, Reader-Adapter, Fehlerpolitik, Restartability, Partitionierung, Integration Channels und AMQP Publisher Confirms.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 33 Spring Batch Job Boundary Job-Konfiguration nur verdrahten; Fachablauf in einen Use Case verschieben und Jobparameter typisieren.

Spring Batch Job Boundary

Code Smell

Job-Konfiguration enthält Fachlogik, Infrastrukturdetails und Laufparameter gleichzeitig.

Pattern / Prinzip

Application Service / Job Facade

Refactoring-Pfad

Job-Konfiguration nur verdrahten; Fachablauf in einen Use Case verschieben und Jobparameter typisieren.

Alternative

Direkter Tasklet nur für sehr kleine administrative Einzelschritte

Vorher

Spring-Ausgangscode
@Bean Job importJob() { return jobs.get("import").start(stepWithBusinessLogic()).build(); }

Nachher

Spring-Zielcode
return importJobFactory.create(new ImportJobParameters(runId, source));

Kompilierbare Referenz

SpringBatchJobBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
// Pattern: Application Service + Job Facade
// Zweck: Spring-Batch-Konfiguration von der fachlichen Importsteuerung trennen.
public final class SpringBatchJobBoundaryRefactoring {
  public record ImportJobParameters(String runId,String source,Instant requestedAt) {
    public ImportJobParameters { if(runId==null||runId.isBlank()) throw new IllegalArgumentException("runId"); }
  }
  public interface ImportUseCase { ImportResult execute(ImportJobParameters parameters); }
  public record ImportResult(int accepted,int rejected) {}
  public static final class ImportJobFacade {
    private final ImportUseCase useCase;
    public ImportJobFacade(ImportUseCase useCase){this.useCase=useCase;}
    public ImportResult run(ImportJobParameters parameters){return useCase.execute(parameters);}
  }
}

Bereich 34 Chunk-Transaktionsgrenzen bewusst schneiden Chunkgröße fachlich und technisch begründen; jeder Chunk bildet eine abgeschlossene Unit of Work.

Chunk-Transaktionsgrenzen bewusst schneiden

Code Smell

Ein kompletter Datenimport läuft in einer einzigen langen Transaktion.

Pattern / Prinzip

Chunk Processing / Unit of Work

Refactoring-Pfad

Chunkgröße fachlich und technisch begründen; jeder Chunk bildet eine abgeschlossene Unit of Work.

Alternative

Tasklet bei einem atomaren Einzelschritt ohne Datenstrom

Vorher

Spring-Ausgangscode
@Transactional void importAll(List<Row> rows) { rows.forEach(this::save); }

Nachher

Spring-Zielcode
for (var chunk : chunks.of(rows, 100)) unitOfWork.commit(process(chunk));

Kompilierbare Referenz

ChunkTransactionBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.ArrayList; import java.util.List;
// Pattern: Chunk Processing + Unit of Work
// Zweck: Große Batchläufe in restartbare, kurze Transaktionen zerlegen.
public final class ChunkTransactionBoundaryRefactoring {
  public interface UnitOfWork<T> { void commit(List<T> items); }
  public static <T> int process(List<T> items,int chunkSize,UnitOfWork<T> unit){
    if(chunkSize<1) throw new IllegalArgumentException("chunkSize");
    int committed=0;
    for(int from=0;from<items.size();from+=chunkSize){
      var chunk=new ArrayList<>(items.subList(from,Math.min(items.size(),from+chunkSize)));
      unit.commit(List.copyOf(chunk)); committed+=chunk.size();
    }
    return committed;
  }
}

Bereich 35 ItemReader als Infrastruktur-Adapter Technisches Eingabeformat am Rand lesen und sofort in ein stabiles fachliches Input-Modell übersetzen.

ItemReader als Infrastruktur-Adapter

Code Smell

Reader liefert JPA-Entities oder CSV-Zeilen direkt bis in die Fachlogik.

Pattern / Prinzip

Adapter / Anti-Corruption Layer

Refactoring-Pfad

Technisches Eingabeformat am Rand lesen und sofort in ein stabiles fachliches Input-Modell übersetzen.

Alternative

Direktes Mapping bei trivialem, internem Einmalimport

Vorher

Spring-Ausgangscode
ItemReader<CustomerEntity> reader = repositoryReader();

Nachher

Spring-Zielcode
CustomerImportItem item = readerPort.readNext().map(mapper::toDomain);

Kompilierbare Referenz

ItemReaderAdapterRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Optional;
// Pattern: Adapter + Anti-Corruption Layer
// Zweck: Technische Eingabeformate vor der Batch-Fachlogik normalisieren.
public final class ItemReaderAdapterRefactoring {
  public record CsvRow(String externalId,String name,String country) {}
  public record CustomerImportItem(String customerId,String displayName,String countryCode) {}
  public interface RowSource { Optional<CsvRow> next(); }
  public interface ItemReaderPort { Optional<CustomerImportItem> readNext(); }
  public static final class CsvItemReaderAdapter implements ItemReaderPort {
    private final RowSource source;
    public CsvItemReaderAdapter(RowSource source){this.source=source;}
    public Optional<CustomerImportItem> readNext(){
      return source.next().map(r->new CustomerImportItem(r.externalId().trim(),r.name().trim(),r.country().toUpperCase()));
    }
  }
}

Bereich 36 Skip und Retry nach Fehlerklasse Transient, fachlich ungültig und permanent unterscheiden; Retry-Budget und Skip-Gründe explizit modellieren.

Skip und Retry nach Fehlerklasse

Code Smell

Jeder Fehler wird gleich behandelt: entweder kompletter Abbruch oder blindes Wiederholen.

Pattern / Prinzip

Policy / Error Classification

Refactoring-Pfad

Transient, fachlich ungültig und permanent unterscheiden; Retry-Budget und Skip-Gründe explizit modellieren.

Alternative

Fail-fast bei regulatorisch atomaren Läufen

Vorher

Spring-Ausgangscode
catch (Exception e) { retry(); }

Nachher

Spring-Zielcode
return failurePolicy.decide(error, attempt);

Kompilierbare Referenz

SkipRetryPolicyRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Policy + Error Classification
// Zweck: Skip, Retry und Abbruch nachvollziehbar nach Fehlerklasse entscheiden.
public final class SkipRetryPolicyRefactoring {
  public enum FailureKind { TRANSIENT, INVALID_ITEM, PERMANENT }
  public enum Decision { RETRY, SKIP, FAIL_JOB }
  public interface Classifier { FailureKind classify(Throwable error); }
  public static final class FailurePolicy {
    private final Classifier classifier; private final int maxAttempts;
    public FailurePolicy(Classifier classifier,int maxAttempts){this.classifier=classifier;this.maxAttempts=maxAttempts;}
    public Decision decide(Throwable error,int attempt){
      return switch(classifier.classify(error)){
        case TRANSIENT -> attempt<maxAttempts ? Decision.RETRY : Decision.FAIL_JOB;
        case INVALID_ITEM -> Decision.SKIP;
        case PERMANENT -> Decision.FAIL_JOB;
      };
    }
  }
}

Bereich 37 Restartability und Checkpoint-Fortschritt Fortschritt als stabilen Checkpoint speichern; fachlichen Business-Key statt Listenindex verwenden.

Restartability und Checkpoint-Fortschritt

Code Smell

Neustart beginnt wieder bei Datensatz 1 oder verlässt sich auf flüchtigen Speicher.

Pattern / Prinzip

Checkpoint / Memento

Refactoring-Pfad

Fortschritt als stabilen Checkpoint speichern; fachlichen Business-Key statt Listenindex verwenden.

Alternative

Vollständiger Neuaufbau bei kleinen, idempotenten Datenmengen

Vorher

Spring-Ausgangscode
int currentIndex = 0;

Nachher

Spring-Zielcode
checkpointStore.save(new Checkpoint(jobId, lastBusinessKey));

Kompilierbare Referenz

BatchRestartabilityRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Optional;
// Pattern: Checkpoint + Memento
// Zweck: Batchläufe nach Fehlern ohne doppelte Fachwirkung fortsetzen.
public final class BatchRestartabilityRefactoring {
  public record Checkpoint(String jobId,String lastBusinessKey,long processedCount) {}
  public interface CheckpointStore { Optional<Checkpoint> load(String jobId); void save(Checkpoint checkpoint); }
  public static final class ProgressTracker {
    private final CheckpointStore store;
    public ProgressTracker(CheckpointStore store){this.store=store;}
    public Optional<String> resumeAfter(String jobId){return store.load(jobId).map(Checkpoint::lastBusinessKey);}
    public void processed(String jobId,String businessKey,long count){store.save(new Checkpoint(jobId,businessKey,count));}
  }
}

Bereich 38 Partitionierung ohne geteilten Zustand PartitionContext unveränderlich machen; jede Partition besitzt eigene Ports und liefert ein Ergebnisobjekt zurück.

Partitionierung ohne geteilten Zustand

Code Smell

Parallele Partitionen schreiben in gemeinsame mutable Listen oder Zähler.

Pattern / Prinzip

Partitioned Work / Shared-Nothing

Refactoring-Pfad

PartitionContext unveränderlich machen; jede Partition besitzt eigene Ports und liefert ein Ergebnisobjekt zurück.

Alternative

Single-threaded Chunking bei kleinen Datenmengen

Vorher

Spring-Ausgangscode
static final List<Result> results = new ArrayList<>();

Nachher

Spring-Zielcode
PartitionResult result = worker.execute(new PartitionContext(rangeStart, rangeEnd));

Kompilierbare Referenz

BatchPartitioningRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Partitioned Work + Shared-Nothing
// Zweck: Batch-Partitionen unabhängig, deterministisch und parallel ausführbar halten.
public final class BatchPartitioningRefactoring {
  public record PartitionContext(String partitionId,long fromInclusive,long toExclusive) {
    public PartitionContext { if(toExclusive<fromInclusive) throw new IllegalArgumentException("range"); }
  }
  public record PartitionResult(String partitionId,long processed,List<String> errors) {
    public PartitionResult { errors=List.copyOf(errors); }
  }
  public interface PartitionWorker { PartitionResult execute(PartitionContext context); }
  public static long total(List<PartitionResult> results){return results.stream().mapToLong(PartitionResult::processed).sum();}
}

Bereich 39 Spring Integration Channel als Port Fachlichen Gateway-Port definieren; Spring-Integration-Adapter baut Message und Header erst am Rand.

Spring Integration Channel als Port

Code Smell

Fachservice kennt MessageBuilder, Header-Namen und konkrete Channel-Beans.

Pattern / Prinzip

Messaging Gateway / Port

Refactoring-Pfad

Fachlichen Gateway-Port definieren; Spring-Integration-Adapter baut Message und Header erst am Rand.

Alternative

Direkter Methodenaufruf innerhalb desselben Moduls

Vorher

Spring-Ausgangscode
messageChannel.send(MessageBuilder.withPayload(order).setHeader("tenant", tenant).build());

Nachher

Spring-Zielcode
dispatchPort.dispatch(new DispatchCommand(orderId, tenantId));

Kompilierbare Referenz

SpringIntegrationChannelBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Messaging Gateway + Port
// Zweck: Spring-Integration-APIs aus der Fachlogik heraus halten.
public final class SpringIntegrationChannelBoundaryRefactoring {
  public record DispatchCommand(String orderId,String tenantId) {}
  public record MessageEnvelope(Object payload,Map<String,String> headers) {}
  public interface DispatchPort { void dispatch(DispatchCommand command); }
  public interface MessageChannel { boolean send(MessageEnvelope message); }
  public static final class IntegrationChannelAdapter implements DispatchPort {
    private final MessageChannel channel;
    public IntegrationChannelAdapter(MessageChannel channel){this.channel=channel;}
    public void dispatch(DispatchCommand command){
      if(!channel.send(new MessageEnvelope(command,Map.of("tenantId",command.tenantId())))) throw new IllegalStateException("channel rejected message");
    }
  }
}

Bereich 40 AMQP Publisher Confirm und Zustellstatus Publish-Versuch und Confirm getrennt modellieren; Zustellstatus erst nach Broker-Bestätigung auf CONFIRMED setzen.

AMQP Publisher Confirm und Zustellstatus

Code Smell

Nach publish() gilt eine Nachricht sofort als erfolgreich zugestellt.

Pattern / Prinzip

Reliable Messaging / Publisher Confirm

Refactoring-Pfad

Publish-Versuch und Confirm getrennt modellieren; Zustellstatus erst nach Broker-Bestätigung auf CONFIRMED setzen.

Alternative

Transactional Outbox für stärkere Persistenzkopplung

Vorher

Spring-Ausgangscode
rabbitTemplate.convertAndSend(exchange, key, event); markSent(event.id());

Nachher

Spring-Zielcode
receipt = publisher.publish(envelope); deliveryStore.confirm(receipt.messageId());

Kompilierbare Referenz

AmqpPublisherConfirmRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
// Pattern: Reliable Messaging + Publisher Confirm
// Zweck: Brokerannahme explizit bestätigen und Zustellstatus nachvollziehbar führen.
public final class AmqpPublisherConfirmRefactoring {
  public record OutboundMessage(String id,String exchange,String routingKey,byte[] payload) {}
  public record PublishReceipt(String messageId,boolean accepted,Instant confirmedAt) {}
  public interface BrokerPublisher { PublishReceipt publish(OutboundMessage message); }
  public interface DeliveryStore { void markConfirmed(String messageId,Instant confirmedAt); void markRejected(String messageId); }
  public static boolean publish(OutboundMessage message,BrokerPublisher broker,DeliveryStore store){
    var receipt=broker.publish(message);
    if(receipt.accepted()){store.markConfirmed(receipt.messageId(),receipt.confirmedAt());return true;}
    store.markRejected(receipt.messageId());return false;
  }
}
Kapitel 6 Security- und Testarchitektur8 Fachthemen

Security- und Testgrenzen sauber schneiden: OAuth2 Resource Server, Claims Mapping, CORS/CSRF, Spring Session, Test Slices, MockMvc-Verträge, Testcontainers und ArchUnit.

Spring-Themen · Abschnitt 6

Security- und Testarchitektur

Security- und Testgrenzen sauber schneiden: OAuth2 Resource Server, Claims Mapping, CORS/CSRF, Spring Session, Test Slices, MockMvc-Verträge, Testcontainers und ArchUnit.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 41 OAuth2 Resource Server Boundary Claims am HTTP-Rand in einen typisierten ActorContext übersetzen; der Use Case bleibt Spring-Security-frei.

OAuth2 Resource Server Boundary

Code Smell

Controller und Fachcode lesen direkt rohe JWT-Claims.

Pattern / Prinzip

Security Adapter / Authentication Boundary

Refactoring-Pfad

Claims am HTTP-Rand in einen typisierten ActorContext übersetzen; der Use Case bleibt Spring-Security-frei.

Alternative

Direkte Annotationen nur für sehr einfache Rollenprüfung

Vorher

Spring-Ausgangscode
JwtAuthenticationToken token = (JwtAuthenticationToken) authentication;

Nachher

Spring-Zielcode
useCase.execute(command, actorContextMapper.from(authentication));

Kompilierbare Referenz

OAuth2ResourceServerBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map; import java.util.Set;
// Pattern: Security Adapter + Authentication Boundary
// Zweck: OAuth2/JWT-Details am Web-Rand in einen stabilen Fachkontext übersetzen.
public final class OAuth2ResourceServerBoundaryRefactoring {
  public record ActorContext(String subject, Set<String> roles, String tenantId) { public ActorContext { roles=Set.copyOf(roles); } }
  public record JwtPrincipal(String subject, Map<String,Object> claims) { public JwtPrincipal { claims=Map.copyOf(claims); } }
  public static ActorContext map(JwtPrincipal jwt){
    var raw=jwt.claims().getOrDefault("roles", java.util.List.of());
    var roles=((java.util.Collection<?>)raw).stream().map(String::valueOf).collect(java.util.stream.Collectors.toUnmodifiableSet());
    return new ActorContext(jwt.subject(),roles,String.valueOf(jwt.claims().getOrDefault("tenant","default")));
  }
}

Bereich 42 JWT Claims Mapping als Anti-Corruption Layer Provider-Claims in ein internes IdentityProfile normalisieren und fehlende Pflichtclaims explizit ablehnen.

JWT Claims Mapping als Anti-Corruption Layer

Code Smell

Claim-Namen und Provider-Sonderfälle verteilen sich über mehrere Services.

Pattern / Prinzip

Anti-Corruption Layer / Mapper

Refactoring-Pfad

Provider-Claims in ein internes IdentityProfile normalisieren und fehlende Pflichtclaims explizit ablehnen.

Alternative

Direktes Mapping bei einem stabilen internen Tokenformat

Vorher

Spring-Ausgangscode
String email = jwt.getClaim("mail"); String tenant = jwt.getClaim("tid");

Nachher

Spring-Zielcode
IdentityProfile profile = claimsMapper.map(tokenClaims);

Kompilierbare Referenz

JwtClaimsMappingRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Anti-Corruption Layer + Mapper
// Zweck: Provider-spezifische Claim-Namen in ein internes Identitätsmodell normalisieren.
public final class JwtClaimsMappingRefactoring {
  public record IdentityProfile(String userId,String email,String tenantId) {}
  public interface ClaimsMapper { IdentityProfile map(Map<String,Object> claims); }
  public static final class DefaultClaimsMapper implements ClaimsMapper {
    public IdentityProfile map(Map<String,Object> c){
      return new IdentityProfile(required(c,"sub"), first(c,"email","mail"), first(c,"tenant_id","tid"));
    }
    private static String required(Map<String,Object> c,String k){var v=c.get(k);if(v==null||String.valueOf(v).isBlank())throw new IllegalArgumentException("missing "+k);return String.valueOf(v);}
    private static String first(Map<String,Object> c,String a,String b){var v=c.get(a);return v!=null?String.valueOf(v):required(c,b);}
  }
}

Bereich 43 CORS und CSRF als getrennte Security Boundaries Browser-Ursprünge, Cookie-Nutzung und zustandsändernde Requests getrennt modellieren; Policy am Adapter testen.

CORS und CSRF als getrennte Security Boundaries

Code Smell

CORS und CSRF werden als ein gemeinsamer Schalter behandelt oder global deaktiviert.

Pattern / Prinzip

Boundary Policy / Defense in Depth

Refactoring-Pfad

Browser-Ursprünge, Cookie-Nutzung und zustandsändernde Requests getrennt modellieren; Policy am Adapter testen.

Alternative

CSRF deaktivieren nur bei wirklich stateless Bearer-Token-APIs

Vorher

Spring-Ausgangscode
http.cors().and().csrf().disable();

Nachher

Spring-Zielcode
securityPolicy.evaluate(new BrowserRequest(origin, method, cookieAuth));

Kompilierbare Referenz

CorsCsrfBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set;
// Pattern: Boundary Policy + Defense in Depth
// Zweck: CORS-Herkunft und CSRF-Risiko getrennt und testbar entscheiden.
public final class CorsCsrfBoundaryRefactoring {
  public record BrowserRequest(String origin,String method,boolean cookieAuthenticated) {}
  public record Decision(boolean corsAllowed,boolean csrfTokenRequired) {}
  public static final class SecurityPolicy {
    private final Set<String> allowedOrigins;
    public SecurityPolicy(Set<String> origins){allowedOrigins=Set.copyOf(origins);}
    public Decision evaluate(BrowserRequest r){
      boolean cors=allowedOrigins.contains(r.origin());
      boolean changing=!Set.of("GET","HEAD","OPTIONS").contains(r.method());
      return new Decision(cors,r.cookieAuthenticated()&&changing);
    }
  }
}

Bereich 44 Spring Session hinter einem Session Port Einen typisierten SessionPort definieren; Web-/Redis-Details bleiben im Adapter und Sessiondaten erhalten Ablaufregeln.

Spring Session hinter einem Session Port

Code Smell

Fachservices greifen direkt auf HttpSession und String-Attribute zu.

Pattern / Prinzip

Port & Adapter / Session Repository

Refactoring-Pfad

Einen typisierten SessionPort definieren; Web-/Redis-Details bleiben im Adapter und Sessiondaten erhalten Ablaufregeln.

Alternative

Stateless Token bei vollständig zustandslosen APIs

Vorher

Spring-Ausgangscode
session.setAttribute("cart", cart);

Nachher

Spring-Zielcode
sessionPort.save(new UserSession(sessionId, cartId, expiresAt));

Kompilierbare Referenz

SpringSessionBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant; import java.util.Optional;
// Pattern: Port & Adapter + Session Repository
// Zweck: HttpSession/Redis-Details von fachlichen Sitzungsdaten trennen.
public final class SpringSessionBoundaryRefactoring {
  public record UserSession(String sessionId,String cartId,Instant expiresAt) { public boolean expired(Instant now){return !expiresAt.isAfter(now);} }
  public interface SessionPort { Optional<UserSession> find(String id); void save(UserSession session); void delete(String id); }
  public static Optional<UserSession> active(SessionPort port,String id,Instant now){
    var found=port.find(id); found.filter(s->s.expired(now)).ifPresent(s->port.delete(id)); return found.filter(s->!s.expired(now));
  }
}

Bereich 45 Test Slices nach Adaptergrenzen Tests nach Adaptergrenze schneiden: reine Unit Tests, Web Slice, Data Slice und wenige End-to-End-Tests.

Test Slices nach Adaptergrenzen

Code Smell

@SpringBootTest startet für jede kleine Mapper- oder Controller-Prüfung den gesamten Kontext.

Pattern / Prinzip

Test Pyramid / Slice Boundary

Refactoring-Pfad

Tests nach Adaptergrenze schneiden: reine Unit Tests, Web Slice, Data Slice und wenige End-to-End-Tests.

Alternative

Vollkontexttest für echte Verdrahtungs- und Startprüfungen

Vorher

Spring-Ausgangscode
@SpringBootTest class PriceControllerTest { ... }

Nachher

Spring-Zielcode
WebSliceHarness harness = new WebSliceHarness(controller, exceptionMapper);

Kompilierbare Referenz

SpringTestSliceRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.function.Function;
// Pattern: Test Pyramid + Slice Boundary
// Zweck: Kleine Spring-Adapter isoliert und schnell prüfen, ohne den Vollkontext zu starten.
public final class SpringTestSliceRefactoring {
  public record Request(String body) {} public record Response(int status,String body) {}
  public interface Controller { Response handle(Request request); }
  public static final class WebSliceHarness {
    private final Controller controller; private final Function<RuntimeException,Response> errors;
    public WebSliceHarness(Controller c,Function<RuntimeException,Response> e){controller=c;errors=e;}
    public Response perform(Request r){try{return controller.handle(r);}catch(RuntimeException ex){return errors.apply(ex);}}
  }
}

Bereich 46 MockMvc Contract Tests statt Implementierungsdetails HTTP-Vertrag über Status, Header, Problemformat und JSON-Schema testen; interne Umsetzung austauschbar halten.

MockMvc Contract Tests statt Implementierungsdetails

Code Smell

Tests prüfen interne Serviceaufrufe, private Methoden oder konkrete JSON-Mapper-Details.

Pattern / Prinzip

Contract Test / Black-Box Adapter Test

Refactoring-Pfad

HTTP-Vertrag über Status, Header, Problemformat und JSON-Schema testen; interne Umsetzung austauschbar halten.

Alternative

Direkter Unit Test für reine Controller-Mappingfunktionen

Vorher

Spring-Ausgangscode
verify(service).create(any());

Nachher

Spring-Zielcode
contract.assertResponse(exchange, expectedStatus, requiredHeaders);

Kompilierbare Referenz

MockMvcContractRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map; import java.util.Set;
// Pattern: Contract Test + Black-Box Adapter Test
// Zweck: Stabilen HTTP-Vertrag statt interner Aufrufreihenfolgen prüfen.
public final class MockMvcContractRefactoring {
  public record HttpExchange(int status,Map<String,String> headers,Map<String,Object> body) { public HttpExchange { headers=Map.copyOf(headers); body=Map.copyOf(body); } }
  public record Contract(int status,Set<String> requiredHeaders,Set<String> requiredFields) {}
  public static boolean matches(HttpExchange x,Contract c){return x.status()==c.status()&&x.headers().keySet().containsAll(c.requiredHeaders())&&x.body().keySet().containsAll(c.requiredFields());}
}

Bereich 47 Testcontainers als Infrastructure Fixture Container-Lebenszyklus und dynamische Verbindungsdaten in einer Fixture kapseln; Tests erhalten isolierte Ressourcen.

Testcontainers als Infrastructure Fixture

Code Smell

Integrationstests hängen von lokal installierten Datenbanken oder gemeinsam genutzten Testsystemen ab.

Pattern / Prinzip

Infrastructure Fixture / Test Data Builder

Refactoring-Pfad

Container-Lebenszyklus und dynamische Verbindungsdaten in einer Fixture kapseln; Tests erhalten isolierte Ressourcen.

Alternative

In-Memory-Fake für reine Port-Vertragstests

Vorher

Spring-Ausgangscode
jdbcUrl = "jdbc:postgresql://localhost:5432/test";

Nachher

Spring-Zielcode
try (var fixture = databaseFixture.start()) { repositoryContract.verify(fixture.connection()); }

Kompilierbare Referenz

TestcontainersBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Infrastructure Fixture + Test Data Builder
// Zweck: Flüchtige Infrastruktur testweise kapseln und dynamische Eigenschaften explizit liefern.
public final class TestcontainersBoundaryRefactoring {
  public record ConnectionInfo(String jdbcUrl,String username,String password) {}
  public interface ManagedFixture extends AutoCloseable { ConnectionInfo start(); Map<String,String> properties(); void close(); }
  public static final class RepositoryContract {
    public boolean verify(ManagedFixture fixture){
      var connection=fixture.start();
      try{return connection.jdbcUrl().startsWith("jdbc:")&&!connection.username().isBlank();}
      finally{fixture.close();}
    }
  }
}

Bereich 48 ArchUnit-Regeln für Spring-Schichten Architekturregeln als ausführbare Fitness Functions modellieren: Controller -> Application -> Domain, keine umgekehrten Abhängigkeiten.

ArchUnit-Regeln für Spring-Schichten

Code Smell

Schichtengrenzen existieren nur in Dokumentation und werden schleichend verletzt.

Pattern / Prinzip

Architecture Test / Fitness Function

Refactoring-Pfad

Architekturregeln als ausführbare Fitness Functions modellieren: Controller -> Application -> Domain, keine umgekehrten Abhängigkeiten.

Alternative

JPMS-Modulgrenzen für stärkere Compile-Time-Kapselung

Vorher

Spring-Ausgangscode
controller imports repository.entity.CustomerEntity;

Nachher

Spring-Zielcode
architectureRules.verify(dependencies);

Kompilierbare Referenz

ArchUnitSpringLayersRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Architecture Test + Fitness Function
// Zweck: Spring-Schichtengrenzen automatisiert und dauerhaft absichern.
public final class ArchUnitSpringLayersRefactoring {
  public record Dependency(String sourceLayer,String targetLayer,String sourceType,String targetType) {}
  public record Violation(Dependency dependency,String reason) {}
  public static List<Violation> verify(List<Dependency> dependencies){
    return dependencies.stream().filter(d->
      (d.sourceLayer().equals("domain")&&!d.targetLayer().equals("domain")) ||
      (d.sourceLayer().equals("web")&&d.targetLayer().equals("persistence")))
      .map(d->new Violation(d,"unerlaubte Schichtabhängigkeit")).toList();
  }
}
Kapitel 7 Betriebsreife und Observability8 Fachthemen

Operational Excellence ohne Framework-Leaks: Health, Kubernetes-Probes, Observability, Tracing, Feature Flags, Environment-Komposition, Graceful Shutdown und explizite Async-Ausführung.

Spring-Themen · Abschnitt 7

Betriebsreife und Observability

Operational Excellence ohne Framework-Leaks: Health, Kubernetes-Probes, Observability, Tracing, Feature Flags, Environment-Komposition, Graceful Shutdown und explizite Async-Ausführung.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 49 Actuator Health Indicator als fachlicher Port Diagnoseabhängigkeiten hinter kleinen Ports kapseln und Status mit Ursache sowie Degradationsgrad modellieren.

Actuator Health Indicator als fachlicher Port

Code Smell

Health-Checks greifen direkt auf Repository und Fremdsysteme zu und vermischen Diagnose mit Fachlogik.

Pattern / Prinzip

Health Contributor / Diagnostic Port

Refactoring-Pfad

Diagnoseabhängigkeiten hinter kleinen Ports kapseln und Status mit Ursache sowie Degradationsgrad modellieren.

Alternative

Ein einfacher Ping für unkritische interne Tools

Vorher

Spring-Ausgangscode
repository.count(); externalClient.ping();

Nachher

Spring-Zielcode
HealthSnapshot health = healthService.snapshot();

Kompilierbare Referenz

ActuatorHealthBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Diagnostic Port + Health Contributor
// Zweck: Technische Diagnose hinter stabilen Ports bündeln, ohne Fachservices zu missbrauchen.
public final class ActuatorHealthBoundaryRefactoring {
  public enum Status { UP, DEGRADED, DOWN }
  public record DependencyHealth(String name, Status status, String detail) {}
  public record HealthSnapshot(Status status, List<DependencyHealth> dependencies) {
    public HealthSnapshot { dependencies = List.copyOf(dependencies); }
  }
  public interface HealthProbe { DependencyHealth check(); }
  public static HealthSnapshot snapshot(List<HealthProbe> probes) {
    var results = probes.stream().map(HealthProbe::check).toList();
    var overall = results.stream().anyMatch(x -> x.status()==Status.DOWN) ? Status.DOWN
      : results.stream().anyMatch(x -> x.status()==Status.DEGRADED) ? Status.DEGRADED : Status.UP;
    return new HealthSnapshot(overall, results);
  }
}

Bereich 50 Liveness und Readiness bewusst trennen Liveness nur für Prozessfähigkeit verwenden; Readiness prüft, ob neue Arbeit sicher angenommen werden kann.

Liveness und Readiness bewusst trennen

Code Smell

Ein einziger Health-Endpunkt führt dazu, dass temporär fehlende Abhängigkeiten den Prozess unnötig neu starten.

Pattern / Prinzip

Probe Separation / Operational Boundary

Refactoring-Pfad

Liveness nur für Prozessfähigkeit verwenden; Readiness prüft, ob neue Arbeit sicher angenommen werden kann.

Alternative

Ein kombinierter Check bei nicht orchestrierten Anwendungen

Vorher

Spring-Ausgangscode
if (!databaseUp) System.exit(1);

Nachher

Spring-Zielcode
ProbeDecision decision = probes.evaluate(processState, dependencies);

Kompilierbare Referenz

LivenessReadinessRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Probe Separation + Operational Boundary
// Zweck: Neustartentscheidung und Traffic-Freigabe semantisch trennen.
public final class LivenessReadinessRefactoring {
  public record ProcessState(boolean eventLoopResponsive, boolean fatalError) {}
  public record Dependency(String name, boolean required, boolean available) {}
  public record ProbeDecision(boolean live, boolean ready, List<String> blockers) {
    public ProbeDecision { blockers = List.copyOf(blockers); }
  }
  public static ProbeDecision evaluate(ProcessState process, List<Dependency> dependencies) {
    boolean live = process.eventLoopResponsive() && !process.fatalError();
    var blockers = dependencies.stream().filter(d -> d.required() && !d.available()).map(Dependency::name).toList();
    return new ProbeDecision(live, live && blockers.isEmpty(), blockers);
  }
}

Bereich 51 Micrometer Observation als Decorator Messung um den Use Case legen; Fachcode kennt nur Operation und Ergebnis, nicht das Metrikframework.

Micrometer Observation als Decorator

Code Smell

Metrikaufrufe liegen verstreut in Use Cases und koppeln Fachcode an konkrete Meter-Namen.

Pattern / Prinzip

Decorator / Observation Port

Refactoring-Pfad

Messung um den Use Case legen; Fachcode kennt nur Operation und Ergebnis, nicht das Metrikframework.

Alternative

Direkte Timer bei sehr kleinen technischen Adaptern

Vorher

Spring-Ausgangscode
timer.record(() -> service.execute(command));

Nachher

Spring-Zielcode
return observedUseCase.execute(command);

Kompilierbare Referenz

MicrometerObservationRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration;
// Pattern: Decorator + Observation Port
// Zweck: Metriken und Laufzeitmessung außerhalb des fachlichen Use Cases halten.
public final class MicrometerObservationRefactoring {
  public interface UseCase<C,R> { R execute(C command); }
  public interface ObservationPort { <R> R observe(String operation, java.util.function.Supplier<R> work); }
  public static final class ObservedUseCase<C,R> implements UseCase<C,R> {
    private final String operation; private final UseCase<C,R> delegate; private final ObservationPort observations;
    public ObservedUseCase(String operation, UseCase<C,R> delegate, ObservationPort observations){this.operation=operation;this.delegate=delegate;this.observations=observations;}
    public R execute(C command){ return observations.observe(operation, () -> delegate.execute(command)); }
  }
  public record Measurement(String operation, Duration duration, boolean success) {}
}

Bereich 52 Tracing Context explizit propagieren TraceContext als unveränderliches Objekt durch Ports und asynchrone Tasks reichen.

Tracing Context explizit propagieren

Code Smell

Trace- und Tenant-Kontext steckt in ThreadLocal und geht bei Async- oder Virtual-Thread-Grenzen verloren.

Pattern / Prinzip

Context Object / Explicit Propagation

Refactoring-Pfad

TraceContext als unveränderliches Objekt durch Ports und asynchrone Tasks reichen.

Alternative

Framework-Kontext nur innerhalb eines synchronen HTTP-Adapters

Vorher

Spring-Ausgangscode
MDC.put("traceId", request.getHeader("X-Trace-Id"));

Nachher

Spring-Zielcode
executor.submit(() -> useCase.execute(command, traceContext));

Kompilierbare Referenz

TracingContextPropagationRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Context Object + Explicit Propagation
// Zweck: Trace-, Tenant- und Korrelationsdaten unabhängig von ThreadLocal weitergeben.
public final class TracingContextPropagationRefactoring {
  public record TraceContext(String traceId, String spanId, String tenantId, Map<String,String> baggage) {
    public TraceContext { baggage = Map.copyOf(baggage); }
    public TraceContext child(String newSpanId){ return new TraceContext(traceId,newSpanId,tenantId,baggage); }
  }
  public interface ContextualTask<R> { R run(TraceContext context); }
  public static <R> R execute(ContextualTask<R> task, TraceContext context){ return task.run(context); }
}

Bereich 53 Feature Flags als typisierte Policy Flagzugriff zentralisieren und fachliche Entscheidung als typisierte Policy ausdrücken.

Feature Flags als typisierte Policy

Code Smell

Stringbasierte Flags werden überall abgefragt und erzeugen versteckte alternative Geschäftslogik.

Pattern / Prinzip

Policy / Feature Toggle Boundary

Refactoring-Pfad

Flagzugriff zentralisieren und fachliche Entscheidung als typisierte Policy ausdrücken.

Alternative

Branch by Abstraction für langfristige Migrationen

Vorher

Spring-Ausgangscode
if (flags.isEnabled("new-price-v2")) { ... }

Nachher

Spring-Zielcode
PricingPolicy policy = rolloutPolicy.forCustomer(customer);

Kompilierbare Referenz

TypedFeatureFlagRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set;
// Pattern: Policy + Feature Toggle Boundary
// Zweck: Rolloutentscheidungen typisieren und aus der Fachlogik herauslösen.
public final class TypedFeatureFlagRefactoring {
  public enum Feature { NEW_PRICING, ASYNC_FULFILLMENT, STRICT_VALIDATION }
  public record CustomerContext(String customerId, String tenantId, Set<String> segments) { public CustomerContext { segments=Set.copyOf(segments); } }
  public interface FeaturePolicy { boolean enabled(Feature feature, CustomerContext context); }
  public static final class SegmentPolicy implements FeaturePolicy {
    private final Set<String> enabledSegments;
    public SegmentPolicy(Set<String> enabledSegments){this.enabledSegments=Set.copyOf(enabledSegments);}
    public boolean enabled(Feature feature, CustomerContext context){return context.segments().stream().anyMatch(enabledSegments::contains);}
  }
}

Bereich 54 Profile-freie Environment Strategy Umgebungsfähigkeiten zentral ermitteln und dort eine passende Adapterstrategie zusammensetzen.

Profile-freie Environment Strategy

Code Smell

@Profile verteilt Infrastrukturentscheidungen über viele Beans und macht Kombinationen schwer nachvollziehbar.

Pattern / Prinzip

Strategy / Composition Root

Refactoring-Pfad

Umgebungsfähigkeiten zentral ermitteln und dort eine passende Adapterstrategie zusammensetzen.

Alternative

@Profile für klar getrennte Demo- oder Testkonfigurationen

Vorher

Spring-Ausgangscode
@Profile("prod") class S3DocumentStore { }

Nachher

Spring-Zielcode
DocumentStore store = environmentStrategy.documentStore(capabilities);

Kompilierbare Referenz

EnvironmentStrategyRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set;
// Pattern: Strategy + Composition Root
// Zweck: Adapterwahl zentral aus Fähigkeiten ableiten statt Annotationen zu verstreuen.
public final class EnvironmentStrategyRefactoring {
  public enum Capability { OBJECT_STORAGE, LOCAL_DISK, ENCRYPTION, LOW_LATENCY }
  public record Environment(Set<Capability> capabilities) { public Environment { capabilities=Set.copyOf(capabilities); } }
  public interface DocumentStore { String kind(); }
  public static DocumentStore select(Environment env) {
    if (env.capabilities().contains(Capability.OBJECT_STORAGE)) return () -> "object-storage";
    if (env.capabilities().contains(Capability.LOCAL_DISK)) return () -> "local-disk";
    throw new IllegalStateException("no supported document store");
  }
}

Bereich 55 Graceful Shutdown mit Drain Policy Neue Arbeit stoppen, laufende Operationen zählen, Frist anwenden und danach Ressourcen geordnet schließen.

Graceful Shutdown mit Drain Policy

Code Smell

Shutdown beendet Executor und Messaging-Consumer abrupt; laufende Aufträge bleiben inkonsistent.

Pattern / Prinzip

Lifecycle Coordinator / Drain Policy

Refactoring-Pfad

Neue Arbeit stoppen, laufende Operationen zählen, Frist anwenden und danach Ressourcen geordnet schließen.

Alternative

Sofortiger Shutdown bei stateless und idempotenten Worker-Prozessen

Vorher

Spring-Ausgangscode
executor.shutdownNow();

Nachher

Spring-Zielcode
shutdownCoordinator.drain(deadline);

Kompilierbare Referenz

GracefulShutdownRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
// Pattern: Lifecycle Coordinator + Drain Policy
// Zweck: Neue Arbeit sperren und laufende Operationen vor Ressourcenfreigabe kontrolliert beenden.
public final class GracefulShutdownRefactoring {
  public static final class WorkGate {
    private final AtomicInteger active = new AtomicInteger(); private volatile boolean accepting = true;
    public AutoCloseable enter(){ if(!accepting) throw new IllegalStateException("draining"); active.incrementAndGet(); return active::decrementAndGet; }
    public void stopAccepting(){ accepting=false; }
    public int active(){ return active.get(); }
  }
  public static boolean drain(WorkGate gate, Duration timeout) throws InterruptedException {
    gate.stopAccepting(); long end=System.nanoTime()+timeout.toNanos();
    while(gate.active()>0 && System.nanoTime()<end) Thread.sleep(10);
    return gate.active()==0;
  }
}

Bereich 56 @Async durch expliziten Executor-Port ersetzen Asynchrone Ausführung über einen benannten Port modellieren und Fehler sowie Completion explizit zurückgeben.

@Async durch expliziten Executor-Port ersetzen

Code Smell

@Async versteckt Threadwechsel, Fehlerkanal und Executor-Auswahl; Tests werden timingabhängig.

Pattern / Prinzip

Executor Port / Command

Refactoring-Pfad

Asynchrone Ausführung über einen benannten Port modellieren und Fehler sowie Completion explizit zurückgeben.

Alternative

Direkte synchrone Ausführung bei kurzen CPU-Operationen

Vorher

Spring-Ausgangscode
@Async public void sendMail(Command command) { ... }

Nachher

Spring-Zielcode
CompletionStage<Result> result = taskExecutor.submit(command);

Kompilierbare Referenz

AsyncExecutorBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
// Pattern: Executor Port + Command
// Zweck: Threadwechsel, Fehlerkanal und Executor-Ownership explizit machen.
public final class AsyncExecutorBoundaryRefactoring {
  public record Command(String id, String payload) {}
  public record Result(String id, boolean accepted, String detail) {}
  public interface TaskExecutorPort { CompletionStage<Result> submit(Command command); }
  public static final class ExplicitExecutor implements TaskExecutorPort {
    private final java.util.concurrent.Executor executor; private final Function<Command,Result> handler;
    public ExplicitExecutor(java.util.concurrent.Executor executor, Function<Command,Result> handler){this.executor=executor;this.handler=handler;}
    public CompletionStage<Result> submit(Command command){ return CompletableFuture.supplyAsync(() -> handler.apply(command), executor); }
  }
}
Kapitel 8 Modulgrenzen und zuverlässige Events8 Fachthemen

Modulgrenzen, zuverlässige Events und versionierte Verträge.

Spring-Themen · Abschnitt 8

Modulgrenzen und zuverlässige Events

Modulgrenzen, zuverlässige Events und versionierte Verträge.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 57 Domain Events an Modulgrenzen Fachereignisse im Quellmodul erzeugen und über einen kleinen Event-Port nach erfolgreichem Commit publizieren.

Domain Events an Modulgrenzen

Code Smell

Direkte Service-Kaskaden koppeln Module und machen Transaktionsfolgen unsichtbar.

Pattern / Prinzip

Domain Event + Modulgrenze

Refactoring-Pfad

Fachereignisse im Quellmodul erzeugen und über einen kleinen Event-Port nach erfolgreichem Commit publizieren.

Alternative

Direkter Aufruf bei streng lokalem, synchronem Verhalten

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
ModuleDomainEventRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ModuleDomainEventRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
import java.util.List;
// Pattern: Domain Event + Module Boundary
// Zweck: Fachliche Folgeaktionen von der Transaktionslogik des Quellmoduls entkoppeln.
public final class ModuleDomainEventRefactoring {
  public sealed interface DomainEvent permits OrderApproved { Instant occurredAt(); }
  public record OrderApproved(String orderId, Instant occurredAt) implements DomainEvent {}
  public record Decision(boolean approved, List<DomainEvent> events) { public Decision { events=List.copyOf(events); } }
  public static Decision approve(String orderId) { return new Decision(true, List.of(new OrderApproved(orderId, Instant.now()))); }
}

Bereich 58 Outbox Poller als Infrastruktur-Adapter Ereignis atomar mit Fachdaten speichern; separater Poller übernimmt Zustellung und markiert den Eintrag.

Outbox Poller als Infrastruktur-Adapter

Code Smell

Geschäftstransaktion und Broker-Publish bilden einen unsicheren Dual Write.

Pattern / Prinzip

Transactional Outbox + Polling Publisher

Refactoring-Pfad

Ereignis atomar mit Fachdaten speichern; separater Poller übernimmt Zustellung und markiert den Eintrag.

Alternative

Direkter Publish bei rein best-effort Benachrichtigungen

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
OutboxPollerRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

OutboxPollerRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Transactional Outbox + Polling Publisher
// Zweck: Broker-Zustellung von der Fachtransaktion trennen, ohne Ereignisse zu verlieren.
public final class OutboxPollerRefactoring {
  public record OutboxEntry(long id, String type, String payload) {}
  public interface OutboxStore { List<OutboxEntry> claim(int limit); void delivered(long id); }
  public interface EventPublisher { void publish(OutboxEntry event); }
  public static int poll(OutboxStore store, EventPublisher publisher, int limit) {
    int sent=0; for (var e:store.claim(limit)) { publisher.publish(e); store.delivered(e.id()); sent++; } return sent;
  }
}

Bereich 59 Inbox Pattern für idempotente Verarbeitung Message-ID vor Fachausführung atomar reservieren und Ergebnis beziehungsweise Status persistieren.

Inbox Pattern für idempotente Verarbeitung

Code Smell

Wiederholte Nachrichten lösen dieselbe Fachaktion mehrfach aus.

Pattern / Prinzip

Inbox + Idempotent Consumer

Refactoring-Pfad

Message-ID vor Fachausführung atomar reservieren und Ergebnis beziehungsweise Status persistieren.

Alternative

Natürlich idempotente Operation ohne Seiteneffekte

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
InboxConsumerRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

InboxConsumerRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Inbox + Idempotent Consumer
// Zweck: At-least-once-Zustellung in genau eine fachliche Ausführung übersetzen.
public final class InboxConsumerRefactoring {
  public record Message(String id, String payload) {}
  public interface Inbox { boolean reserve(String messageId); void complete(String messageId); }
  public interface Handler { void handle(String payload); }
  public static boolean consume(Message message, Inbox inbox, Handler handler) {
    if (!inbox.reserve(message.id())) return false;
    handler.handle(message.payload()); inbox.complete(message.id()); return true;
  }
}

Bereich 60 Spring Retry nach Fehlerklasse Fehler in transient, permanent und fachlich klassifizieren; nur transiente Fehler mit Budget wiederholen.

Spring Retry nach Fehlerklasse

Code Smell

Globale Retries wiederholen auch fachliche Ablehnungen und vervielfachen Last.

Pattern / Prinzip

Retry Policy + Error Classification

Refactoring-Pfad

Fehler in transient, permanent und fachlich klassifizieren; nur transiente Fehler mit Budget wiederholen.

Alternative

Kein Retry bei synchroner Benutzerkorrektur

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
RetryClassificationRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

RetryClassificationRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Retry Policy + Error Classification
// Zweck: Wiederholung nur für tatsächlich transiente Fehler zulassen.
public final class RetryClassificationRefactoring {
  public enum FailureKind { TRANSIENT, PERMANENT, BUSINESS }
  public record Failure(FailureKind kind, String message) {}
  public record RetryDecision(boolean retry, int delayMillis) {}
  public static RetryDecision decide(Failure failure, int attempt) {
    if (failure.kind()!=FailureKind.TRANSIENT || attempt>=4) return new RetryDecision(false,0);
    return new RetryDecision(true, Math.min(2000, 100 << attempt));
  }
}

Bereich 61 Konfigurierbare Resilience Pipeline Resilienzschritte als geordnete, typisierte Policy konfigurieren und zentral validieren.

Konfigurierbare Resilience Pipeline

Code Smell

Retry, Timeout und Circuit Breaker werden ungeordnet und mehrfach verschachtelt.

Pattern / Prinzip

Pipeline + Policy Object

Refactoring-Pfad

Resilienzschritte als geordnete, typisierte Policy konfigurieren und zentral validieren.

Alternative

Ein einzelner Timeout bei sehr einfachem Adapter

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
ResiliencePipelineRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ResiliencePipelineRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration;
import java.util.List;
// Pattern: Pipeline + Policy Object
// Zweck: Reihenfolge und Budget von Resilienzmechanismen explizit machen.
public final class ResiliencePipelineRefactoring {
  public sealed interface Step permits Timeout, Retry, CircuitBreaker {}
  public record Timeout(Duration value) implements Step {}
  public record Retry(int maxAttempts) implements Step {}
  public record CircuitBreaker(int failureThreshold) implements Step {}
  public record Policy(List<Step> steps) { public Policy { steps=List.copyOf(steps); } }
  public static Policy externalApiDefault() { return new Policy(List.of(new Timeout(Duration.ofSeconds(2)), new Retry(3), new CircuitBreaker(5))); }
}

Bereich 62 Spring Modulith als ausführbare Modulgrenze Fachmodule mit expliziten APIs schneiden und Abhängigkeiten als ausführbare Regeln prüfen.

Spring Modulith als ausführbare Modulgrenze

Code Smell

Package-Konventionen sind dokumentiert, aber nicht automatisch geprüft.

Pattern / Prinzip

Modular Monolith + Fitness Function

Refactoring-Pfad

Fachmodule mit expliziten APIs schneiden und Abhängigkeiten als ausführbare Regeln prüfen.

Alternative

ArchUnit-Regeln ohne Spring Modulith

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
SpringModulithBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

SpringModulithBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set;
// Pattern: Modular Monolith + Fitness Function
// Zweck: Erlaubte Modulabhängigkeiten als überprüfbares Modell festhalten.
public final class SpringModulithBoundaryRefactoring {
  public record Module(String name, Set<String> allowedDependencies) { public Module { allowedDependencies=Set.copyOf(allowedDependencies); } }
  public static boolean dependencyAllowed(Module source, String target) { return source.allowedDependencies().contains(target); }
  public static Module orders() { return new Module("orders", Set.of("customers", "shared")); }
}

Bereich 63 Typisierte Event Contracts Events als unveränderliche Records mit Versionsfeld und klarer Semantik modellieren.

Typisierte Event Contracts

Code Smell

String-basierte Eventnamen und Maps verstecken Schemafehler bis zur Laufzeit.

Pattern / Prinzip

Typed Contract + Published Language

Refactoring-Pfad

Events als unveränderliche Records mit Versionsfeld und klarer Semantik modellieren.

Alternative

Schema Registry mit generierten Klassen

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
TypedEventContractRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

TypedEventContractRefactoring.java
package com.aydinsude.workbench.spring;
import java.math.BigDecimal;
import java.time.Instant;
// Pattern: Typed Contract + Published Language
// Zweck: Eventschema im Compiler sichtbar und versionierbar machen.
public final class TypedEventContractRefactoring {
  public record Money(BigDecimal amount, String currency) {}
  public record PaymentCapturedV1(String eventId, String paymentId, Money amount, Instant occurredAt, int schemaVersion) {
    public PaymentCapturedV1 { if (schemaVersion!=1) throw new IllegalArgumentException("schemaVersion"); }
  }
}

Bereich 64 Versionierte REST API mit Deprecation Policy Versionen explizit routen, Sunset-Zeitpunkt dokumentieren und Adapter auf gemeinsamen Use Case abbilden.

Versionierte REST API mit Deprecation Policy

Code Smell

Breaking Changes werden still in bestehende Endpunkte eingebaut.

Pattern / Prinzip

API Versioning + Deprecation Policy

Refactoring-Pfad

Versionen explizit routen, Sunset-Zeitpunkt dokumentieren und Adapter auf gemeinsamen Use Case abbilden.

Alternative

Additive Evolution ohne neue Version

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
VersionedRestApiRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

VersionedRestApiRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.LocalDate;
// Pattern: API Versioning + Deprecation Policy
// Zweck: Externe Verträge kontrolliert entwickeln und intern auf einen Use Case normalisieren.
public final class VersionedRestApiRefactoring {
  public record V1Request(String customerId, long cents) {}
  public record Command(String customerId, long minorUnits, String currency) {}
  public record Deprecation(String version, LocalDate sunset) {}
  public static Command adapt(V1Request request) { return new Command(request.customerId(), request.cents(), "EUR"); }
  public static Deprecation v1Policy() { return new Deprecation("v1", LocalDate.of(2027,1,31)); }
}
Kapitel 9 AOT, Konfiguration und Adaptergrenzen8 Fachthemen

AOT, Konfiguration und moderne Spring-Adaptergrenzen.

Spring-Themen · Abschnitt 9

AOT, Konfiguration und Adaptergrenzen

AOT, Konfiguration und moderne Spring-Adaptergrenzen.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 65 AOT-freundliche Reflection Boundary Reflektive Typen an einer Stelle registrieren und Kernlogik auf statische Verträge umstellen.

AOT-freundliche Reflection Boundary

Code Smell

Dynamische Klassenauflösung und breite Reflection verhindern sichere Native-Images.

Pattern / Prinzip

Reflection Boundary + Explicit Registry

Refactoring-Pfad

Reflektive Typen an einer Stelle registrieren und Kernlogik auf statische Verträge umstellen.

Alternative

JVM-only Betrieb ohne AOT-Ziel

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
AotReflectionBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

AotReflectionBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
import java.util.function.Supplier;
// Pattern: Reflection Boundary + Explicit Registry
// Zweck: Dynamische Typauflösung aus dem Kern entfernen und AOT-Analyse ermöglichen.
public final class AotReflectionBoundaryRefactoring {
  public interface Decoder { Object decode(String value); }
  public record Registry(Map<String,Supplier<Decoder>> decoders) { public Registry { decoders=Map.copyOf(decoders); } }
  public static Decoder decoder(Registry registry, String type) { var f=registry.decoders().get(type); if(f==null) throw new IllegalArgumentException(type); return f.get(); }
}

Bereich 66 Runtime Hints als zentraler Adapter AOT-Metadaten zentral aus technischen Adapteranforderungen ableiten.

Runtime Hints als zentraler Adapter

Code Smell

Reflection-, Resource- und Proxy-Hinweise liegen verstreut neben Fachklassen.

Pattern / Prinzip

Metadata Adapter + Composition Root

Refactoring-Pfad

AOT-Metadaten zentral aus technischen Adapteranforderungen ableiten.

Alternative

Keine Runtime Hints bei vollständig statischem Code

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
RuntimeHintsBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

RuntimeHintsBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Set;
// Pattern: Metadata Adapter + Composition Root
// Zweck: AOT-Runtime-Metadaten zentral und testbar beschreiben.
public final class RuntimeHintsBoundaryRefactoring {
  public record RuntimeHints(Set<Class<?>> reflectionTypes, Set<String> resources) { public RuntimeHints { reflectionTypes=Set.copyOf(reflectionTypes); resources=Set.copyOf(resources); } }
  public static RuntimeHints documentAdapterHints() { return new RuntimeHints(Set.of(DocumentPayload.class), Set.of("schemas/document-v1.json")); }
  public record DocumentPayload(String id, String content) {}
}

Bereich 67 Conditional Beans durch explizite Komposition ersetzen Adapterauswahl aus typisierter Umgebung zentral zusammensetzen und validieren.

Conditional Beans durch explizite Komposition ersetzen

Code Smell

Viele @Conditional-Varianten machen den aktiven Objektgraphen schwer nachvollziehbar.

Pattern / Prinzip

Composition Root + Strategy Selection

Refactoring-Pfad

Adapterauswahl aus typisierter Umgebung zentral zusammensetzen und validieren.

Alternative

Spring Auto-Configuration für echte Bibliotheken

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
ExplicitBeanCompositionRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ExplicitBeanCompositionRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Composition Root + Strategy Selection
// Zweck: Aktiven Adaptergraph explizit und ohne verteilte Conditions aufbauen.
public final class ExplicitBeanCompositionRefactoring {
  public enum StorageKind { JDBC, IN_MEMORY }
  public interface Store { String kind(); }
  public static Store compose(StorageKind kind) { return switch(kind) { case JDBC -> ()->"jdbc"; case IN_MEMORY -> ()->"memory"; }; }
}

Bereich 68 Auto-Configuration als Bibliotheksgrenze Nur technische Defaults liefern, bei eigener Bean sauber zurücktreten und Properties validieren.

Auto-Configuration als Bibliotheksgrenze

Code Smell

Starter registrieren unkontrolliert Beans und überschreiben Anwendungspolicies.

Pattern / Prinzip

Auto-Configuration Boundary + Back-off

Refactoring-Pfad

Nur technische Defaults liefern, bei eigener Bean sauber zurücktreten und Properties validieren.

Alternative

Explizite Konfiguration in einer einzelnen Anwendung

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
AutoConfigurationBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

AutoConfigurationBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Auto-Configuration Boundary + Back-off
// Zweck: Wiederverwendbare technische Defaults anbieten, ohne Anwendungshoheit zu übernehmen.
public final class AutoConfigurationBoundaryRefactoring {
  public record ClientProperties(String baseUrl, int timeoutMillis) { public ClientProperties { if(baseUrl==null||baseUrl.isBlank()||timeoutMillis<=0) throw new IllegalArgumentException(); } }
  public interface ExternalClient { String baseUrl(); }
  public static ExternalClient defaultClient(ClientProperties p, ExternalClient userProvided) { return userProvided!=null ? userProvided : p::baseUrl; }
}

Bereich 69 Secrets hinter einem Credentials Port Secret-Auflösung in Infrastruktur kapseln und nur kurzlebige typisierte Credentials liefern.

Secrets hinter einem Credentials Port

Code Smell

Use Cases lesen Environment-Variablen und Vault-Schlüssel direkt.

Pattern / Prinzip

Credentials Port + Adapter

Refactoring-Pfad

Secret-Auflösung in Infrastruktur kapseln und nur kurzlebige typisierte Credentials liefern.

Alternative

Statische lokale Testkonfiguration

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
CredentialsPortRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

CredentialsPortRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
// Pattern: Credentials Port + Adapter
// Zweck: Secret-Quelle und Rotation aus Fach- und Integrationslogik entfernen.
public final class CredentialsPortRefactoring {
  public record Credentials(String username, char[] secret, Instant expiresAt) { public Credentials { secret=secret.clone(); } public char[] secret(){ return secret.clone(); } }
  public interface CredentialsPort { Credentials current(String system); }
  public static boolean valid(Credentials c, Instant now) { return c.expiresAt().isAfter(now); }
}

Bereich 70 Dynamische Konfiguration als Snapshot Konfiguration atomar als validierten unveränderlichen Snapshot veröffentlichen.

Dynamische Konfiguration als Snapshot

Code Smell

Jeder Codepfad liest veränderliche Konfiguration einzeln und sieht inkonsistente Werte.

Pattern / Prinzip

Immutable Snapshot + Atomic Publication

Refactoring-Pfad

Konfiguration atomar als validierten unveränderlichen Snapshot veröffentlichen.

Alternative

Neustart bei seltenen Konfigurationsänderungen

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
DynamicConfigurationSnapshotRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

DynamicConfigurationSnapshotRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
// Pattern: Immutable Snapshot + Atomic Publication
// Zweck: Konsistente dynamische Konfiguration ohne Teilzustände bereitstellen.
public final class DynamicConfigurationSnapshotRefactoring {
  public record Snapshot(int maxAttempts, Duration timeout) { public Snapshot { if(maxAttempts<1||timeout.isNegative()) throw new IllegalArgumentException(); } }
  public static final class Store { private final AtomicReference<Snapshot> value; public Store(Snapshot initial){value=new AtomicReference<>(initial);} public Snapshot current(){return value.get();} public void publish(Snapshot next){value.set(next);} }
}

Bereich 71 HTTP Interface Client als ausgehender Port Fachlichen Client-Port definieren und deklaratives Spring-Interface nur im Adapter implementieren.

HTTP Interface Client als ausgehender Port

Code Smell

Controller oder Service verwendet framework-spezifischen HTTP-Client direkt.

Pattern / Prinzip

Port + Declarative Client Adapter

Refactoring-Pfad

Fachlichen Client-Port definieren und deklaratives Spring-Interface nur im Adapter implementieren.

Alternative

WebClient-Adapter bei dynamischen Requests

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
HttpInterfaceClientBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

HttpInterfaceClientBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Optional;
// Pattern: Port + Declarative Client Adapter
// Zweck: Spring HTTP Interface außerhalb des Anwendungskerns halten.
public final class HttpInterfaceClientBoundaryRefactoring {
  public record CustomerProfile(String id, String segment) {}
  public interface CustomerDirectoryPort { Optional<CustomerProfile> find(String customerId); }
  public interface DeclarativeHttpClient { CustomerProfile getCustomer(String id); }
  public static CustomerDirectoryPort adapter(DeclarativeHttpClient client) { return id -> Optional.ofNullable(client.getCustomer(id)); }
}

Bereich 72 Thin GraphQL Resolver Boundary Resolver mappt Request auf Query; Batching und Fachlogik liegen in expliziten Ports beziehungsweise Use Cases.

Thin GraphQL Resolver Boundary

Code Smell

Resolver enthält Autorisierung, N+1-Ladevorgänge und Fachentscheidungen.

Pattern / Prinzip

Thin Resolver + DataLoader Port

Refactoring-Pfad

Resolver mappt Request auf Query; Batching und Fachlogik liegen in expliziten Ports beziehungsweise Use Cases.

Alternative

REST-Endpunkt bei einfacher Ressourcenabfrage

Vorher

Spring-Ausgangscode
frameworkApi.call(); service.save(); eventBus.publish();

Nachher

Spring-Zielcode
GraphqlResolverBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

GraphqlResolverBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
import java.util.Map;
// Pattern: Thin Resolver + DataLoader Port
// Zweck: GraphQL-Transport, Batching und Fachabfrage sauber trennen.
public final class GraphqlResolverBoundaryRefactoring {
  public record OrderView(String id, String customerId) {}
  public interface OrderQuery { List<OrderView> byCustomer(String customerId); }
  public interface CustomerBatchLoader { Map<String,String> names(List<String> customerIds); }
  public static List<OrderView> resolveOrders(String customerId, OrderQuery query) { return query.byCustomer(customerId); }
}
Kapitel 10 Reaktive Systeme und modulare Komposition8 Fachthemen

Reaktive Grenzen, Backpressure, Streaming, AOP, Lifecycle und die finale modulare Komposition.

Spring-Themen · Abschnitt 10

Reaktive Systeme und modulare Komposition

Reaktive Grenzen, Backpressure, Streaming, AOP, Lifecycle und die finale modulare Komposition.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Bereich 73 Reactive Handler Boundary Handler normalisiert den Request und delegiert an einen frameworkfreien Use Case.

Reactive Handler Boundary

Code Smell

WebFlux-Handler enthält Mapping, Fachlogik und technische Fehlerbehandlung in einer reaktiven Kette.

Pattern / Prinzip

Thin Reactive Adapter + Use Case Port

Refactoring-Pfad

Handler normalisiert den Request und delegiert an einen frameworkfreien Use Case.

Alternative

Klassisches MVC bei ausschließlich blockierender Verarbeitung

Vorher

Spring-Ausgangscode
return repository.save(request).map(this::toResponse);

Nachher

Spring-Zielcode
ReactiveHandlerBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ReactiveHandlerBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.concurrent.CompletionStage;
// Pattern: Thin Reactive Adapter + Use Case Port
// Zweck: WebFlux-Transport von fachlicher Verarbeitung und Antwortmodell trennen.
public final class ReactiveHandlerBoundaryRefactoring {
  public record CreateOrderCommand(String customerId, long amountCents) {}
  public record OrderResult(String orderId, String status) {}
  public interface CreateOrderUseCase { CompletionStage<OrderResult> execute(CreateOrderCommand command); }
  public static CompletionStage<OrderResult> handle(String customerId, long amountCents, CreateOrderUseCase useCase) {
    if (customerId == null || customerId.isBlank()) throw new IllegalArgumentException("customerId");
    return useCase.execute(new CreateOrderCommand(customerId, amountCents));
  }
}

Bereich 74 Blocking Call Isolation in Reactive Flow Blockierende Arbeit hinter einem expliziten Port auf einen kontrollierten Executor verschieben.

Blocking Call Isolation in Reactive Flow

Code Smell

Blockierende Datenbank- oder Dateiaufrufe laufen unbemerkt auf Event-Loop-Threads.

Pattern / Prinzip

Blocking Adapter + Executor Port

Refactoring-Pfad

Blockierende Arbeit hinter einem expliziten Port auf einen kontrollierten Executor verschieben.

Alternative

Vollständig nichtblockierender Treiber und reaktive Persistenz

Vorher

Spring-Ausgangscode
return eventLoop.submit(() -> legacyStore.load(id));

Nachher

Spring-Zielcode
ReactiveBlockingIsolationRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ReactiveBlockingIsolationRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletionStage;
// Pattern: Blocking Adapter + Executor Port
// Zweck: Blockierende I/O bewusst aus reaktiven Event-Loops isolieren.
public final class ReactiveBlockingIsolationRefactoring {
  public interface LegacyCustomerStore { String loadName(String customerId); }
  public static CompletionStage<String> loadAsync(String customerId, LegacyCustomerStore store, Executor blockingExecutor) {
    return CompletableFuture.supplyAsync(() -> store.loadName(customerId), blockingExecutor);
  }
}

Bereich 75 R2DBC Transaction Boundary Transaktion um einen vollständigen Use Case legen und externe Veröffentlichung nach Commit ausführen.

R2DBC Transaction Boundary

Code Smell

Reaktive Transaktion wird über Controller, Repository und Event-Publisher verteilt.

Pattern / Prinzip

Reactive Unit of Work

Refactoring-Pfad

Transaktion um einen vollständigen Use Case legen und externe Veröffentlichung nach Commit ausführen.

Alternative

JDBC-Transaktion bei bewusst blockierendem Modul

Vorher

Spring-Ausgangscode
repository.save(account); publisher.publish(event);

Nachher

Spring-Zielcode
ReactiveTransactionBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ReactiveTransactionBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.concurrent.CompletionStage;
import java.util.function.Supplier;
// Pattern: Reactive Unit of Work
// Zweck: Reaktive Transaktionsgrenze um einen vollständigen Use Case bündeln.
public final class ReactiveTransactionBoundaryRefactoring {
  public interface ReactiveUnitOfWork { <T> CompletionStage<T> inTransaction(Supplier<CompletionStage<T>> work); }
  public interface AccountRepository { CompletionStage<Void> save(String accountId, long balanceCents); }
  public static CompletionStage<Void> update(String id, long balance, ReactiveUnitOfWork uow, AccountRepository repository) {
    return uow.inTransaction(() -> repository.save(id, balance));
  }
}

Bereich 76 Backpressure and Overflow Policy Kapazität, Overflow-Verhalten und Messgrößen als explizite Policy definieren.

Backpressure and Overflow Policy

Code Smell

Unbegrenzte Ereignisströme sammeln Daten im Speicher, bis Latenz und Heap kollabieren.

Pattern / Prinzip

Backpressure Policy + Bounded Buffer

Refactoring-Pfad

Kapazität, Overflow-Verhalten und Messgrößen als explizite Policy definieren.

Alternative

Synchroner Request-Response-Fluss bei geringer Last

Vorher

Spring-Ausgangscode
events.buffer().subscribe(this::consume);

Nachher

Spring-Zielcode
ReactiveBackpressurePolicyRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ReactiveBackpressurePolicyRefactoring.java
package com.aydinsude.workbench.spring;
// Pattern: Backpressure Policy + Bounded Buffer
// Zweck: Verhalten bei schneller Produktion und langsamer Verarbeitung explizit machen.
public final class ReactiveBackpressurePolicyRefactoring {
  public enum OverflowAction { REJECT_NEWEST, DROP_OLDEST, FAIL_STREAM }
  public record BackpressurePolicy(int capacity, OverflowAction overflowAction) {
    public BackpressurePolicy { if (capacity < 1) throw new IllegalArgumentException("capacity"); }
  }
  public static boolean canAccept(int queued, BackpressurePolicy policy) { return queued < policy.capacity(); }
}

Bereich 77 Server-Sent Events as Outbound Port Fachlichen Ereignisstrom typisieren; SSE-Formatierung bleibt im Web-Adapter.

Server-Sent Events as Outbound Port

Code Smell

SSE-Controller liest direkt Broker-Nachrichten und formatiert Fachereignisse.

Pattern / Prinzip

Event Stream Port + Presenter

Refactoring-Pfad

Fachlichen Ereignisstrom typisieren; SSE-Formatierung bleibt im Web-Adapter.

Alternative

Polling-Endpunkt bei seltenen Aktualisierungen

Vorher

Spring-Ausgangscode
broker.messages().map(this::toSse);

Nachher

Spring-Zielcode
ServerSentEventsBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ServerSentEventsBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Instant;
import java.util.concurrent.Flow;
// Pattern: Event Stream Port + Presenter
// Zweck: Fachereignisstrom von SSE-Transport und Wire-Format trennen.
public final class ServerSentEventsBoundaryRefactoring {
  public record OrderStatusChanged(String orderId, String status, Instant occurredAt) {}
  public interface OrderEventStream { Flow.Publisher<OrderStatusChanged> subscribe(String customerId); }
  public record SseMessage(String id, String event, String data) {}
  public static SseMessage present(OrderStatusChanged event) {
    return new SseMessage(event.orderId(), "order-status", event.status());
  }
}

Bereich 78 AOP Cross-Cutting Boundary Nur technische Querschnittsfunktionen als explizite Decorators modellieren; Fachregeln bleiben im Use Case.

AOP Cross-Cutting Boundary

Code Smell

Aspekte enthalten Fachentscheidungen, verändern Rückgabewerte oder verstecken wichtige Seiteneffekte.

Pattern / Prinzip

Decorator + Explicit Interceptor

Refactoring-Pfad

Nur technische Querschnittsfunktionen als explizite Decorators modellieren; Fachregeln bleiben im Use Case.

Alternative

Direkter Aufruf bei nur einem einfachen Anwendungsfall

Vorher

Spring-Ausgangscode
@Around("execution(* service..*(..))") Object advice(){ ... }

Nachher

Spring-Zielcode
AopBoundaryRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

AopBoundaryRefactoring.java
package com.aydinsude.workbench.spring;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
// Pattern: Decorator + Explicit Interceptor
// Zweck: Messung und Logging ohne versteckte fachliche Wirkung ergänzen.
public final class AopBoundaryRefactoring {
  public interface UseCase<I,O> { O execute(I input); }
  public interface MetricSink { void record(String operation, Duration duration, boolean success); }
  public static <I,O> UseCase<I,O> measured(String operation, UseCase<I,O> delegate, MetricSink metrics, Clock clock) {
    return input -> { Instant start=clock.instant(); boolean success=false; try { O result=delegate.execute(input); success=true; return result; } finally { metrics.record(operation, Duration.between(start, clock.instant()), success); } };
  }
}

Bereich 79 Application Lifecycle Coordinator Initialisierung, Warm-up, Readiness und Shutdown in einem expliziten Zustandsmodell koordinieren.

Application Lifecycle Coordinator

Code Smell

Startup Runner, Scheduler und Listener starten unabhängig und melden Bereitschaft zu früh.

Pattern / Prinzip

Lifecycle Coordinator + Readiness Gate

Refactoring-Pfad

Initialisierung, Warm-up, Readiness und Shutdown in einem expliziten Zustandsmodell koordinieren.

Alternative

Einfacher synchroner Start bei kleiner Anwendung

Vorher

Spring-Ausgangscode
@PostConstruct void startEverything(){ ... }

Nachher

Spring-Zielcode
ApplicationLifecycleCoordinatorRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

ApplicationLifecycleCoordinatorRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.List;
// Pattern: Lifecycle Coordinator + Readiness Gate
// Zweck: Geordnetes Starten, Bereitschaft und Stoppen mehrerer Adapter koordinieren.
public final class ApplicationLifecycleCoordinatorRefactoring {
  public interface Component { void start(); void stop(); boolean ready(); }
  public static final class Coordinator {
    private final List<Component> components;
    public Coordinator(List<Component> components){ this.components=List.copyOf(components); }
    public void start(){ components.forEach(Component::start); }
    public boolean ready(){ return components.stream().allMatch(Component::ready); }
    public void stop(){ for(int i=components.size()-1;i>=0;i--) components.get(i).stop(); }
  }
}

Bereich 80 Spring Composition Root and Feature Modules Pro Feature einen klaren Modulvertrag definieren und die finale Verdrahtung zentral zusammensetzen.

Spring Composition Root and Feature Modules

Code Smell

Komponenten-Scan, Konfiguration und Adapter liegen projektweit verteilt; Modulgrenzen sind nur Konvention.

Pattern / Prinzip

Composition Root + Package by Feature + Architecture Fitness Function

Refactoring-Pfad

Pro Feature einen klaren Modulvertrag definieren und die finale Verdrahtung zentral zusammensetzen.

Alternative

Kleine Anwendung mit einem einzigen fachlichen Modul

Vorher

Spring-Ausgangscode
@ComponentScan("com.company") class App { }

Nachher

Spring-Zielcode
SpringCompositionRootRefactoring.class // explizite, testbare Boundary

Kompilierbare Referenz

SpringCompositionRootRefactoring.java
package com.aydinsude.workbench.spring;
import java.util.Map;
// Pattern: Composition Root + Package by Feature + Architecture Fitness Function
// Zweck: Finale Spring-Verdrahtung sichtbar machen und Featuregrenzen ausführbar halten.
public final class SpringCompositionRootRefactoring {
  public interface OrderPort { String create(String customerId); }
  public interface CustomerPort { boolean exists(String customerId); }
  public record Application(OrderPort orders, CustomerPort customers) {}
  public static Application compose(OrderPort orders, CustomerPort customers) { return new Application(orders, customers); }
  public static boolean allowedDependency(String fromFeature, String toFeature, Map<String, java.util.Set<String>> rules) {
    return fromFeature.equals(toFeature) || rules.getOrDefault(fromFeature, java.util.Set.of()).contains(toFeature);
  }
}
⌂ Cockpit