Abhängigkeiten
Constructor Injection und explizite Konfiguration als Voraussetzung für testbare Komponenten.
Alle zugehörigen Inhalte befinden sich auf dieser einen großen Seite. Kapitel und Beispiele sind standardmäßig geschlossen und lassen sich gezielt öffnen.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Die vorhandenen Kapitel bleiben vollständig erhalten. Diese Orientierung gruppiert sie nach den Entscheidungen, die ein Enterprise-Team tatsächlich treffen muss.
Constructor Injection und explizite Konfiguration als Voraussetzung für testbare Komponenten.
Controller dünn halten und Transaktionsgrenzen in der Anwendungsschicht sichtbar machen.
Spring Data als Adapter behandeln; Domänenmodell und Repository-Vertrag schützen.
Events, Retry und externe Clients mit Idempotenz, Timeout und fachlicher Fehlerübersetzung verbinden.
Acht zentrale Refactorings, die aus schwer testbarem Spring-Code klar abgegrenzte Application-, Domain- und Adapter-Bausteine machen.
Acht zentrale Refactorings, die aus schwer testbarem Spring-Code klar abgegrenzte Application-, Domain- und Adapter-Bausteine machen.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Field Injection erschwert Unit-Tests und erlaubt unvollständig initialisierte Objekte.
Constructor Injection + Dependency Inversion
Abhängigkeiten im Konstruktor sichtbar machen, final speichern und mit Fake-Port testen.
Field Injection nur in sehr kleinen Prototypen
Spring-Ausgangscode@Service
class RegistrationService {
@Autowired private MailSender mailSender;
}Spring-Zielcode@Service
final class RegistrationService {
private final MailSender mailSender;
RegistrationService(MailSender mailSender) {
this.mailSender = mailSender;
}
}ConstructorInjectionRefactoring.javapackage 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"); }
}
}Viele @Value-Felder verteilen Namen, Defaults und Konvertierung über die Anwendung.
Configuration Object
Properties gruppieren, validieren, immutable übergeben und Konfigurationsfehler früh melden.
Einzelnes @Value für wirklich isolierte Werte
Spring-Ausgangscode@Value("${payment.url}") String url;
@Value("${payment.timeout:2s}") Duration timeout;
@Value("${payment.retries:3}") int retries;Spring-Zielcode@ConfigurationProperties("payment")
@Validated
record PaymentProperties(
@NotBlank String url,
@NotNull @Positive Duration timeout,
@Min(0) int retries) {}TypedConfigurationRefactoring.javapackage 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");
}
}
}Controller mit Validierung, Preisberechnung, Persistenz und Benachrichtigung wird zum God Object.
Hexagonal Architecture + Application Service
Request mappen, Use Case aufrufen, Ergebnis in HTTP-Antwort übersetzen.
Einfaches CRUD ohne Fachlogik
Spring-Ausgangscode@PostMapping("/orders")
ResponseEntity<?> create(@RequestBody OrderRequest request) {
validate(request); calculate(request); repository.save(...); mail(...);
return ResponseEntity.ok(...);
}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));
}ThinControllerRefactoring.javapackage 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));
}
}
}Lokale try/catch-Blöcke duplizieren Statuscodes, Texte und Logging.
Exception Translator
Fachliche Exceptions stabilisieren und mit Controller Advice zentral übersetzen.
Direkte lokale Behandlung bei transportnahen Sonderfällen
Spring-Ausgangscodetry { return service.load(id); }
catch (Exception e) {
return ResponseEntity.status(500).body(e.getMessage());
}Spring-Zielcode@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(CustomerNotFound.class)
ResponseEntity<ProblemDetail> notFound(CustomerNotFound e) {
return ResponseEntity.of(ProblemDetail.forStatusAndDetail(NOT_FOUND, e.getMessage()));
}
}ExceptionTranslationRefactoring.javapackage 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());
}
}@Transactional auf Controller oder privaten Hilfsmethoden macht Grenzen unsichtbar und hält Transaktionen zu lange offen.
Unit of Work + Application Service
Datenbankarbeit bündeln; Remote-I/O vor oder nach der Transaktion ausführen.
Programmatic TransactionTemplate bei dynamischen Grenzen
Spring-Ausgangscode@Transactional
@PostMapping("/orders/{id}/approve")
void approve(@PathVariable String id) {
remoteRiskCheck(id); repository.approve(id); mail.send(id);
}Spring-Zielcode@Service
class ApproveOrderUseCase {
@Transactional
void execute(OrderId id) {
var order = repository.get(id);
repository.save(order.approve());
}
}TransactionBoundaryRefactoring.javapackage 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());
}
}
}JPA-Entities, Lazy-Proxies und Query-APIs dringen bis in Controller und Domain vor.
Repository + Adapter
Fachlichen Repository-Port definieren und JPA-Mapping in den Infrastrukturadapter verschieben.
Direkter Spring-Data-Einsatz in reinem CRUD-Modul
Spring-Ausgangscode@Service
class CustomerService {
@PersistenceContext EntityManager em;
CustomerEntity rename(Long id, String name) { ... }
}Spring-Zielcodeinterface CustomerRepository {
Optional<Customer> find(CustomerId id);
void save(Customer customer);
}
@Repository
class JpaCustomerRepositoryAdapter implements CustomerRepository { ... }RepositoryPortRefactoring.javapackage 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));
}
}
}Controller-If-Ketten vermischen Nullprüfung, Format und Geschäftsregeln.
Boundary Validation + Value Object
Bean Validation am DTO; Invarianten im Value Object oder Aggregate schützen.
Nur Domänenvalidierung bei intern erzeugten Commands
Spring-Ausgangscodeif (request.email() == null || !request.email().contains("@")) ...
if (request.name() == null || request.name().isBlank()) ...
if (blockedDomains.contains(domain(request.email()))) ...Spring-Zielcoderecord RegisterCustomerRequest(
@NotBlank @Email String email,
@NotBlank @Size(max=120) String name) {}
// Fachregel bleibt im Use Case / Value Object.ValidationBoundaryRefactoring.javapackage 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");
}
}
}Use Case ruft Mail, Audit, CRM und Analytics direkt nacheinander auf.
Observer / Application Event
Ereignis nach erfolgreicher Zustandsänderung publizieren und Handler idempotent gestalten.
Direkter synchroner Aufruf bei genau einer zwingenden Folgeaktion
Spring-Ausgangscodevoid approve(Order order) {
repository.save(order.approve());
mail.send(order); audit.write(order); crm.sync(order); metrics.count();
}Spring-Zielcodepublisher.publishEvent(new OrderApproved(order.id(), clock.instant()));
@TransactionalEventListener(phase = AFTER_COMMIT)
void sendConfirmation(OrderApproved event) { ... }ApplicationEventsRefactoring.javapackage 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()));
}
}
}HTTP-Verträge, Security, Query-Design, transaktionale Events, Caching und clusterfähige Jobs als klare, testbare Grenzen.
HTTP-Verträge, Security, Query-Design, transaktionale Events, Caching und clusterfähige Jobs als klare, testbare Grenzen.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Controller gibt JPA-Entities direkt zurück und bindet API, Persistenz und Fachmodell zusammen.
Mapper / Anti-Corruption Boundary
Transport-DTO definieren, Mapping isolieren, Domainmodell unabhängig halten.
Direkte Entity-Ausgabe nur bei internem Prototyp ohne stabilen Vertrag
Spring-Ausgangscode@GetMapping("/{id}")
OrderEntity get(@PathVariable long id) {
return repository.findById(id).orElseThrow();
}Spring-Zielcode@GetMapping("/{id}")
OrderResponse get(@PathVariable OrderId id) {
return mapper.toResponse(query.load(id));
}MvcMappingRefactoring.javapackage 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());
}
}
}Jeder Controller erzeugt eigene Maps oder Textmeldungen mit wechselnden Feldern.
Problem Details / Exception Translator
Fehlercode, Status, Detail und Korrelations-ID zentral abbilden.
Kleine interne API mit exakt einem Fehlerformat
Spring-Ausgangscodecatch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}Spring-Zielcode@ExceptionHandler(OrderStateConflict.class)
ProblemDetail conflict(OrderStateConflict e) {
var p = ProblemDetail.forStatusAndDetail(CONFLICT, e.getMessage());
p.setProperty("code", "ORDER_STATE_CONFLICT");
return p;
}ProblemDetailRefactoring.javapackage 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));
}
}SecurityContext oder JWT-Claims werden tief in Services und Domain weitergereicht.
Security Adapter / Principal Mapper
Framework-Principal am Rand lesen und in ActorContext übersetzen.
Direkte SecurityContext-Nutzung ausschließlich im Webadapter
Spring-Ausgangscodevoid approve(String id) {
Authentication a = SecurityContextHolder.getContext().getAuthentication();
service.approve(id, a);
}Spring-Zielcodevoid approve(String id, Jwt jwt) {
var actor = principalMapper.toActor(jwt);
useCase.execute(id, actor);
}SecurityBoundaryRefactoring.javapackage 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); }
}Viele @PreAuthorize-Ausdrücke duplizieren Geschäftslogik und sind schwer isoliert testbar.
Policy / Specification
Security-Annotation auf grobe Grenze reduzieren und fachliche Entscheidung in Policy verschieben.
Statische Rollenprüfung bei rein technischem Admin-Endpunkt
Spring-Ausgangscode@PreAuthorize("hasRole('SUPERVISOR') or (#order.amount < 1000 and #order.owner != authentication.name)")
void approve(Order order) { ... }Spring-Zielcode@PreAuthorize("isAuthenticated()")
void approve(OrderId id) {
useCase.execute(id, actorProvider.current());
}
// Use Case fragt ApprovalPolicy.MethodSecurityPolicyRefactoring.javapackage 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();
}
}
}findAll plus Filterung im Speicher, N+1-Zugriffe und Entity-Leaks in Reports.
Query Object / CQRS Read Model
Query-Port pro Lesefall definieren und Projektion gezielt befüllen.
Einfaches Repository bei kleinen CRUD-Datenmengen
Spring-Ausgangscodevar orders = orderRepository.findAll();
return orders.stream().filter(o -> inRange(o, from, to))
.map(this::toRow).toList();Spring-Zielcodeinterface RevenueProjection { LocalDate getDay(); long getRevenueMinor(); }
@Query("select ... group by ...")
List<RevenueProjection> revenue(LocalDate from, LocalDate to);SpringDataQueryRefactoring.javapackage 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();
}
}Event-Listener läuft innerhalb der Transaktion und verschickt E-Mail, obwohl später rollback erfolgt.
Transactional Observer / Outbox Boundary
After-Commit-Listener oder Outbox verwenden; Handler idempotent gestalten.
Synchroner Listener für reine In-Memory-Aktualisierung
Spring-Ausgangscode@EventListener
void on(InvoiceIssued e) { mail.send(e.invoiceId()); }
// läuft möglicherweise vor Commit-AbschlussSpring-Zielcode@TransactionalEventListener(phase = AFTER_COMMIT)
void on(InvoiceIssued e) { mail.send(e.invoiceId()); }
// Für garantierte Zustellung: Outbox.TransactionalEventRefactoring.javapackage 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())); }
}
}@Cacheable wird wahllos auf Services verteilt; Keys kollidieren und Änderungen invalidieren nicht.
Cache-Aside / Read-Through
Cache nur am stabilen Query-Port, Version im Key und gezielte Eviction nach Mutation.
Kein Cache ohne gemessenen Engpass
Spring-Ausgangscode@Cacheable("products")
Product load(String id) { return repository.find(id); }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")CacheAbstractionRefactoring.javapackage 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); }
}
}@Scheduled-Methode verarbeitet alles ohne Run-ID, Lock oder Wiederanlaufstrategie.
Scheduler Lock + Idempotent Command
Job als Command modellieren, Run-ID persistieren, Lock klein halten und Arbeit checkpointen.
Einzelinstanziger lokaler Wartungsjob ohne Seiteneffekt
Spring-Ausgangscode@Scheduled(cron="0 */5 * * * *")
void reconcile() { repository.findAllOpen().forEach(this::process); }Spring-Zielcode@Scheduled(cron="${jobs.reconcile.cron}")
@SchedulerLock(name="reconcile", lockAtMostFor="PT5M")
void reconcile() { command.execute(runIdFactory.next()); }SchedulingLockRefactoring.javapackage 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"); }
}
}
}Robuste HTTP- und Integrationsgrenzen: Pagination, Nebenläufigkeit, Uploads, WebClient, Resilienz, Observability und Korrelationskontext.
Robuste HTTP- und Integrationsgrenzen: Pagination, Nebenläufigkeit, Uploads, WebClient, Resilienz, Observability und Korrelationskontext.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Controller lädt komplette Listen oder verwendet Offset-Pagination bei gleichzeitigem Schreiben.
Cursor Pagination / Query Object
Sortierschlüssel definieren, Cursor signieren und Query-Port auf Limit plus Cursor ausrichten.
Offset-Pagination bei kleinen, unveränderlichen Datenbeständen
Spring-Ausgangscode@GetMapping
List<OrderEntity> all() { return repository.findAll(); }Spring-Zielcode@GetMapping
OrderPageResponse page(@RequestParam(required=false) String cursor,
@RequestParam(defaultValue="50") int limit) {
return mapper.toResponse(query.loadAfter(cursorCodec.decode(cursor), limit));
}RestPaginationRefactoring.javapackage 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); }
}PUT überschreibt Daten ohne Versionsprüfung; parallele Änderungen gehen unbemerkt verloren.
Optimistic Lock / Conditional Request
Version als ETag ausgeben, If-Match verlangen und Konflikt als 412 oder 409 übersetzen.
Pessimistisches Locking bei kurzer, hochkritischer Transaktion
Spring-Ausgangscode@PutMapping("/{id}")
OrderResponse replace(@PathVariable String id,@RequestBody UpdateOrder body) {
return service.replace(id, body);
}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));
}EtagConcurrencyRefactoring.javapackage 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;}
}
}MultipartFile wird vollständig als byte[] geladen und direkt in Domain oder Datenbank weitergereicht.
Streaming Port / Boundary Object
Metadaten und Stream trennen, Größe und Typ am Rand prüfen, Storage-Port verwenden.
Kleine Konfigurationsdateien mit strengem Größenlimit
Spring-Ausgangscodebyte[] bytes = file.getBytes();
return documentService.store(file.getOriginalFilename(), bytes);Spring-Zielcodevar metadata = mapper.toMetadata(file);
UploadPolicy.validate(metadata);
return ingest.ingest(metadata, file::getInputStream);MultipartBoundaryRefactoring.javapackage 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");
}
}WebClient, URI-Bau, Header und Statuscodes sind direkt im Application Service verteilt.
Outbound Adapter / Anti-Corruption Layer
Fachlichen Port definieren, DTO-Mapping und Fehlerübersetzung im Adapter kapseln.
Direkter Client nur in sehr kleinem technischen Integrationsdienst
Spring-Ausgangscodevar response = webClient.get().uri("/risk/{id}", customerId)
.retrieve().bodyToMono(RemoteRiskResponse.class).block();
return approve(response.score());Spring-ZielcodeRiskScore score = riskAssessment.assess(customerId);
return approvalPolicy.decide(score);WebClientAdapterRefactoring.javapackage 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());
}
}Jeder Fehler wird mehrfach wiederholt; fehlende Timeouts blockieren Threads und verstärken Last.
Retry Policy / Timeout / Idempotency
Fehler klassifizieren, Deadline setzen, Backoff begrenzen und Idempotency-Key mitgeben.
Kein Retry bei fachlichen 4xx-Fehlern oder nicht-idempotenten Aufrufen
Spring-Ausgangscodereturn retryTemplate.execute(ctx -> webClient.post().retrieve().bodyToMono(Result.class).block());Spring-Zielcodereturn resilientClient.call(
Request.withIdempotencyKey(command.id()),
TimeoutBudget.ofSeconds(2),
RetryPolicy.transientFailures(3));RetryTimeoutRefactoring.javapackage 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);} }
}Jeder Request wartet erneut auf denselben ausgefallenen Dienst und erschöpft Thread- und Connection-Pools.
Circuit Breaker / Fallback
Breaker nur im Adapter, Zustände messen, fachlich sinnvollen Fallback definieren.
Retry ohne Circuit Breaker bei sehr seltenen, kurzen Netzwerkfehlern
Spring-Ausgangscodereturn remoteCatalog.load(productId);Spring-Zielcodereturn catalogBreaker.execute(
() -> remoteCatalog.load(productId),
() -> localSnapshot.find(productId));CircuitBreakerRefactoring.javapackage 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;}
}
}Counter und Timer werden überall manuell aufgerufen; Tags haben unkontrollierte Kardinalität.
Decorator / Observability Port
Use Case dekorieren, feste Low-Cardinality-Tags verwenden und Trace-IDs nicht als Metriktag nutzen.
Direkte technische Metrik im reinen Infrastrukturadapter
Spring-Ausgangscoderegistry.counter("orders", "customer", customerId).increment();
return service.place(command);Spring-Zielcodevar observed = new ObservedCommand<>("order.place", useCase, metrics);
return observed.execute(command);ObservabilityRefactoring.javapackage 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;}}
}
}Statisches ThreadLocal verliert Kontext bei Executor- oder Reactive-Wechseln und kann Daten zwischen Requests leaken.
Context Object / Context Propagation
CorrelationContext als Wertobjekt am Rand erzeugen, explizit weitergeben und Adapter für Logs/Headers verwenden.
ThreadLocal ausschließlich in kleinem synchronen Servlet-Adapter mit garantiertem Cleanup
Spring-AusgangscodeCorrelationHolder.set(request.getHeader("X-Correlation-ID"));
executor.submit(() -> service.run(command));Spring-Zielcodevar context = correlationMapper.from(requestHeaders);
executor.submit(() -> useCase.execute(command, context.child()));CorrelationContextRefactoring.javapackage 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); }
}Persistenz und Messaging sauber entkoppeln: Query-Spezifikationen, Fetch Plans, Batch Writes, Outbox, Kafka-Grenzen, Idempotenz, Dead Letters und Schema Evolution.
Persistenz und Messaging sauber entkoppeln: Query-Spezifikationen, Fetch Plans, Batch Writes, Outbox, Kafka-Grenzen, Idempotenz, Dead Letters und Schema Evolution.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Viele abgeleitete Repository-Methoden kombinieren Filtervarianten unübersichtlich.
Specification / Query Object
Filter als typisierte Kriterien modellieren, kombinierbare Spezifikationen erzeugen und das Repository auf eine Suchoperation reduzieren.
Dedizierte Query-Klasse bei sehr komplexen Reporting-Abfragen
Spring-AusgangscodeList<OrderEntity> findByStatusAndRegionAndPriority(String s,String r,int p);Spring-Zielcodevar criteria = new OrderCriteria(status, region, minimumPriority);
return orderQuery.search(criteria);SpringDataSpecificationRefactoring.javapackage 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();}
}Lazy Beziehungen werden in Schleifen unkontrolliert nachgeladen.
Fetch Plan / Data Mapper
Use-Case-spezifische Projektion definieren, Fetch Plan explizit wählen und Aggregate-Grenzen nicht durch globale EAGER-Optionen verwischen.
Batch Fetching bei kontrollierter Objektgraph-Nutzung
Spring-Ausgangscodeorders.forEach(o -> response.add(new View(o.id(), o.customer().name())));Spring-Zielcodereturn orderReadRepository.findSummaries(query);JpaFetchPlanRefactoring.javapackage 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);}
}
}Einzelne save-Aufrufe erzeugen viele Roundtrips und einen wachsenden Persistence Context.
Unit of Work / Batch Writer
Batchgröße festlegen, persistieren, regelmäßig flushen und clear ausführen; fachliche Transaktionsgrenze beibehalten.
JDBC Batch Adapter bei reinem Import ohne Aggregate-Verhalten
Spring-Ausgangscoderows.forEach(repository::save);Spring-ZielcodebatchWriter.write(rows, 100);BatchWriteRefactoring.javapackage 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();
}
}
}Datenbankänderung und Message-Publish bilden einen fehleranfälligen Dual Write.
Transactional Outbox
Fachänderung und Outbox-Eintrag in derselben Transaktion speichern; separater Publisher übernimmt Zustellung und Retry.
CDC-basierte Outbox bei hoher Last
Spring-Ausgangscoderepository.save(order);
kafkaTemplate.send("orders", event);Spring-Zielcodetransaction.execute(() -> { repository.save(order); outbox.append(event); });TransactionalOutboxSpringRefactoring.javapackage 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()));}
}
}Domänen- oder Application-Code hängt direkt an KafkaTemplate, Topicnamen und Headerdetails.
Port & Adapter / Message Envelope
Fachlichen Event-Publisher definieren; Kafka-Adapter übernimmt Serialisierung, Topic-Routing und technische Header.
Direkter KafkaTemplate-Aufruf im kleinen reinen Infrastrukturservice
Spring-AusgangscodekafkaTemplate.send("order-events", orderId, json);Spring-ZielcodedomainEvents.publish(new OrderCompleted(orderId));KafkaProducerBoundaryRefactoring.javapackage 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())));}
}
}Wiederholte Zustellung führt zu doppelten Buchungen oder E-Mails.
Idempotent Consumer / Inbox
Message-ID vor Verarbeitung atomar reservieren; nur der Gewinner verarbeitet; Ergebnis und Status nachvollziehbar speichern.
Natürlich idempotente Set-Operation ohne zusätzliche Inbox
Spring-Ausgangscode@KafkaListener void on(String payload) { bookingService.book(payload); }Spring-Zielcodeif (inbox.reserve(message.id())) handler.handle(message);KafkaConsumerIdempotencyRefactoring.javapackage 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;}
}
}Alle Consumer-Fehler werden gleich oft wiederholt oder kommentarlos verworfen.
Dead Letter Channel / Error Classification
Transient, permanent und poison message unterscheiden; Retry-Budget anwenden; DLQ-Nachricht mit Ursache und Originalmetadaten erzeugen.
Sofortige fachliche Ablehnung bei synchron validierbaren Nachrichten
Spring-Ausgangscodecatch (Exception e) { throw e; }Spring-Zielcodereturn classifier.classify(error) == PERMANENT ? deadLetters.publish(failure) : RETRY;DeadLetterHandlingRefactoring.javapackage 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;
}
}Producer ändert Payloads ohne Versionierungs- oder Kompatibilitätsregel.
Upcaster / Tolerant Reader
Explizite Eventversion transportieren, alte Versionen am Consumer-Rand upcasten und additive Änderungen bevorzugen.
Neues Topic bei semantisch inkompatiblem Ereignis
Spring-Ausgangscoderecord OrderEvent(String id,String customerName) {}Spring-Zielcodevar current = upcasters.toCurrent(envelope.version(), envelope.payload());EventSchemaEvolutionRefactoring.javapackage 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());
}
}
}Spring Batch und Integration sauber schneiden: Job Boundary, Chunk-Transaktionen, Reader-Adapter, Fehlerpolitik, Restartability, Partitionierung, Integration Channels und AMQP Publisher Confirms.
Spring Batch und Integration sauber schneiden: Job Boundary, Chunk-Transaktionen, Reader-Adapter, Fehlerpolitik, Restartability, Partitionierung, Integration Channels und AMQP Publisher Confirms.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Job-Konfiguration enthält Fachlogik, Infrastrukturdetails und Laufparameter gleichzeitig.
Application Service / Job Facade
Job-Konfiguration nur verdrahten; Fachablauf in einen Use Case verschieben und Jobparameter typisieren.
Direkter Tasklet nur für sehr kleine administrative Einzelschritte
Spring-Ausgangscode@Bean Job importJob() { return jobs.get("import").start(stepWithBusinessLogic()).build(); }Spring-Zielcodereturn importJobFactory.create(new ImportJobParameters(runId, source));SpringBatchJobBoundaryRefactoring.javapackage 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);}
}
}Ein kompletter Datenimport läuft in einer einzigen langen Transaktion.
Chunk Processing / Unit of Work
Chunkgröße fachlich und technisch begründen; jeder Chunk bildet eine abgeschlossene Unit of Work.
Tasklet bei einem atomaren Einzelschritt ohne Datenstrom
Spring-Ausgangscode@Transactional void importAll(List<Row> rows) { rows.forEach(this::save); }Spring-Zielcodefor (var chunk : chunks.of(rows, 100)) unitOfWork.commit(process(chunk));ChunkTransactionBoundaryRefactoring.javapackage 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;
}
}Reader liefert JPA-Entities oder CSV-Zeilen direkt bis in die Fachlogik.
Adapter / Anti-Corruption Layer
Technisches Eingabeformat am Rand lesen und sofort in ein stabiles fachliches Input-Modell übersetzen.
Direktes Mapping bei trivialem, internem Einmalimport
Spring-AusgangscodeItemReader<CustomerEntity> reader = repositoryReader();Spring-ZielcodeCustomerImportItem item = readerPort.readNext().map(mapper::toDomain);ItemReaderAdapterRefactoring.javapackage 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()));
}
}
}Jeder Fehler wird gleich behandelt: entweder kompletter Abbruch oder blindes Wiederholen.
Policy / Error Classification
Transient, fachlich ungültig und permanent unterscheiden; Retry-Budget und Skip-Gründe explizit modellieren.
Fail-fast bei regulatorisch atomaren Läufen
Spring-Ausgangscodecatch (Exception e) { retry(); }Spring-Zielcodereturn failurePolicy.decide(error, attempt);SkipRetryPolicyRefactoring.javapackage 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;
};
}
}
}Neustart beginnt wieder bei Datensatz 1 oder verlässt sich auf flüchtigen Speicher.
Checkpoint / Memento
Fortschritt als stabilen Checkpoint speichern; fachlichen Business-Key statt Listenindex verwenden.
Vollständiger Neuaufbau bei kleinen, idempotenten Datenmengen
Spring-Ausgangscodeint currentIndex = 0;Spring-ZielcodecheckpointStore.save(new Checkpoint(jobId, lastBusinessKey));BatchRestartabilityRefactoring.javapackage 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));}
}
}Parallele Partitionen schreiben in gemeinsame mutable Listen oder Zähler.
Partitioned Work / Shared-Nothing
PartitionContext unveränderlich machen; jede Partition besitzt eigene Ports und liefert ein Ergebnisobjekt zurück.
Single-threaded Chunking bei kleinen Datenmengen
Spring-Ausgangscodestatic final List<Result> results = new ArrayList<>();Spring-ZielcodePartitionResult result = worker.execute(new PartitionContext(rangeStart, rangeEnd));BatchPartitioningRefactoring.javapackage 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();}
}Fachservice kennt MessageBuilder, Header-Namen und konkrete Channel-Beans.
Messaging Gateway / Port
Fachlichen Gateway-Port definieren; Spring-Integration-Adapter baut Message und Header erst am Rand.
Direkter Methodenaufruf innerhalb desselben Moduls
Spring-AusgangscodemessageChannel.send(MessageBuilder.withPayload(order).setHeader("tenant", tenant).build());Spring-ZielcodedispatchPort.dispatch(new DispatchCommand(orderId, tenantId));SpringIntegrationChannelBoundaryRefactoring.javapackage 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");
}
}
}Nach publish() gilt eine Nachricht sofort als erfolgreich zugestellt.
Reliable Messaging / Publisher Confirm
Publish-Versuch und Confirm getrennt modellieren; Zustellstatus erst nach Broker-Bestätigung auf CONFIRMED setzen.
Transactional Outbox für stärkere Persistenzkopplung
Spring-AusgangscoderabbitTemplate.convertAndSend(exchange, key, event); markSent(event.id());Spring-Zielcodereceipt = publisher.publish(envelope); deliveryStore.confirm(receipt.messageId());AmqpPublisherConfirmRefactoring.javapackage 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;
}
}Security- und Testgrenzen sauber schneiden: OAuth2 Resource Server, Claims Mapping, CORS/CSRF, Spring Session, Test Slices, MockMvc-Verträge, Testcontainers und ArchUnit.
Security- und Testgrenzen sauber schneiden: OAuth2 Resource Server, Claims Mapping, CORS/CSRF, Spring Session, Test Slices, MockMvc-Verträge, Testcontainers und ArchUnit.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Controller und Fachcode lesen direkt rohe JWT-Claims.
Security Adapter / Authentication Boundary
Claims am HTTP-Rand in einen typisierten ActorContext übersetzen; der Use Case bleibt Spring-Security-frei.
Direkte Annotationen nur für sehr einfache Rollenprüfung
Spring-AusgangscodeJwtAuthenticationToken token = (JwtAuthenticationToken) authentication;Spring-ZielcodeuseCase.execute(command, actorContextMapper.from(authentication));OAuth2ResourceServerBoundaryRefactoring.javapackage 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")));
}
}Claim-Namen und Provider-Sonderfälle verteilen sich über mehrere Services.
Anti-Corruption Layer / Mapper
Provider-Claims in ein internes IdentityProfile normalisieren und fehlende Pflichtclaims explizit ablehnen.
Direktes Mapping bei einem stabilen internen Tokenformat
Spring-AusgangscodeString email = jwt.getClaim("mail"); String tenant = jwt.getClaim("tid");Spring-ZielcodeIdentityProfile profile = claimsMapper.map(tokenClaims);JwtClaimsMappingRefactoring.javapackage 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);}
}
}CORS und CSRF werden als ein gemeinsamer Schalter behandelt oder global deaktiviert.
Boundary Policy / Defense in Depth
Browser-Ursprünge, Cookie-Nutzung und zustandsändernde Requests getrennt modellieren; Policy am Adapter testen.
CSRF deaktivieren nur bei wirklich stateless Bearer-Token-APIs
Spring-Ausgangscodehttp.cors().and().csrf().disable();Spring-ZielcodesecurityPolicy.evaluate(new BrowserRequest(origin, method, cookieAuth));CorsCsrfBoundaryRefactoring.javapackage 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);
}
}
}Fachservices greifen direkt auf HttpSession und String-Attribute zu.
Port & Adapter / Session Repository
Einen typisierten SessionPort definieren; Web-/Redis-Details bleiben im Adapter und Sessiondaten erhalten Ablaufregeln.
Stateless Token bei vollständig zustandslosen APIs
Spring-Ausgangscodesession.setAttribute("cart", cart);Spring-ZielcodesessionPort.save(new UserSession(sessionId, cartId, expiresAt));SpringSessionBoundaryRefactoring.javapackage 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));
}
}@SpringBootTest startet für jede kleine Mapper- oder Controller-Prüfung den gesamten Kontext.
Test Pyramid / Slice Boundary
Tests nach Adaptergrenze schneiden: reine Unit Tests, Web Slice, Data Slice und wenige End-to-End-Tests.
Vollkontexttest für echte Verdrahtungs- und Startprüfungen
Spring-Ausgangscode@SpringBootTest class PriceControllerTest { ... }Spring-ZielcodeWebSliceHarness harness = new WebSliceHarness(controller, exceptionMapper);SpringTestSliceRefactoring.javapackage 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);}}
}
}Tests prüfen interne Serviceaufrufe, private Methoden oder konkrete JSON-Mapper-Details.
Contract Test / Black-Box Adapter Test
HTTP-Vertrag über Status, Header, Problemformat und JSON-Schema testen; interne Umsetzung austauschbar halten.
Direkter Unit Test für reine Controller-Mappingfunktionen
Spring-Ausgangscodeverify(service).create(any());Spring-Zielcodecontract.assertResponse(exchange, expectedStatus, requiredHeaders);MockMvcContractRefactoring.javapackage 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());}
}Integrationstests hängen von lokal installierten Datenbanken oder gemeinsam genutzten Testsystemen ab.
Infrastructure Fixture / Test Data Builder
Container-Lebenszyklus und dynamische Verbindungsdaten in einer Fixture kapseln; Tests erhalten isolierte Ressourcen.
In-Memory-Fake für reine Port-Vertragstests
Spring-AusgangscodejdbcUrl = "jdbc:postgresql://localhost:5432/test";Spring-Zielcodetry (var fixture = databaseFixture.start()) { repositoryContract.verify(fixture.connection()); }TestcontainersBoundaryRefactoring.javapackage 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();}
}
}
}Schichtengrenzen existieren nur in Dokumentation und werden schleichend verletzt.
Architecture Test / Fitness Function
Architekturregeln als ausführbare Fitness Functions modellieren: Controller -> Application -> Domain, keine umgekehrten Abhängigkeiten.
JPMS-Modulgrenzen für stärkere Compile-Time-Kapselung
Spring-Ausgangscodecontroller imports repository.entity.CustomerEntity;Spring-ZielcodearchitectureRules.verify(dependencies);ArchUnitSpringLayersRefactoring.javapackage 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();
}
}Operational Excellence ohne Framework-Leaks: Health, Kubernetes-Probes, Observability, Tracing, Feature Flags, Environment-Komposition, Graceful Shutdown und explizite Async-Ausführung.
Operational Excellence ohne Framework-Leaks: Health, Kubernetes-Probes, Observability, Tracing, Feature Flags, Environment-Komposition, Graceful Shutdown und explizite Async-Ausführung.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Health-Checks greifen direkt auf Repository und Fremdsysteme zu und vermischen Diagnose mit Fachlogik.
Health Contributor / Diagnostic Port
Diagnoseabhängigkeiten hinter kleinen Ports kapseln und Status mit Ursache sowie Degradationsgrad modellieren.
Ein einfacher Ping für unkritische interne Tools
Spring-Ausgangscoderepository.count(); externalClient.ping();Spring-ZielcodeHealthSnapshot health = healthService.snapshot();ActuatorHealthBoundaryRefactoring.javapackage 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);
}
}Ein einziger Health-Endpunkt führt dazu, dass temporär fehlende Abhängigkeiten den Prozess unnötig neu starten.
Probe Separation / Operational Boundary
Liveness nur für Prozessfähigkeit verwenden; Readiness prüft, ob neue Arbeit sicher angenommen werden kann.
Ein kombinierter Check bei nicht orchestrierten Anwendungen
Spring-Ausgangscodeif (!databaseUp) System.exit(1);Spring-ZielcodeProbeDecision decision = probes.evaluate(processState, dependencies);LivenessReadinessRefactoring.javapackage 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);
}
}Metrikaufrufe liegen verstreut in Use Cases und koppeln Fachcode an konkrete Meter-Namen.
Decorator / Observation Port
Messung um den Use Case legen; Fachcode kennt nur Operation und Ergebnis, nicht das Metrikframework.
Direkte Timer bei sehr kleinen technischen Adaptern
Spring-Ausgangscodetimer.record(() -> service.execute(command));Spring-Zielcodereturn observedUseCase.execute(command);MicrometerObservationRefactoring.javapackage 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) {}
}Trace- und Tenant-Kontext steckt in ThreadLocal und geht bei Async- oder Virtual-Thread-Grenzen verloren.
Context Object / Explicit Propagation
TraceContext als unveränderliches Objekt durch Ports und asynchrone Tasks reichen.
Framework-Kontext nur innerhalb eines synchronen HTTP-Adapters
Spring-AusgangscodeMDC.put("traceId", request.getHeader("X-Trace-Id"));Spring-Zielcodeexecutor.submit(() -> useCase.execute(command, traceContext));TracingContextPropagationRefactoring.javapackage 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); }
}Stringbasierte Flags werden überall abgefragt und erzeugen versteckte alternative Geschäftslogik.
Policy / Feature Toggle Boundary
Flagzugriff zentralisieren und fachliche Entscheidung als typisierte Policy ausdrücken.
Branch by Abstraction für langfristige Migrationen
Spring-Ausgangscodeif (flags.isEnabled("new-price-v2")) { ... }Spring-ZielcodePricingPolicy policy = rolloutPolicy.forCustomer(customer);TypedFeatureFlagRefactoring.javapackage 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);}
}
}@Profile verteilt Infrastrukturentscheidungen über viele Beans und macht Kombinationen schwer nachvollziehbar.
Strategy / Composition Root
Umgebungsfähigkeiten zentral ermitteln und dort eine passende Adapterstrategie zusammensetzen.
@Profile für klar getrennte Demo- oder Testkonfigurationen
Spring-Ausgangscode@Profile("prod") class S3DocumentStore { }Spring-ZielcodeDocumentStore store = environmentStrategy.documentStore(capabilities);EnvironmentStrategyRefactoring.javapackage 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");
}
}Shutdown beendet Executor und Messaging-Consumer abrupt; laufende Aufträge bleiben inkonsistent.
Lifecycle Coordinator / Drain Policy
Neue Arbeit stoppen, laufende Operationen zählen, Frist anwenden und danach Ressourcen geordnet schließen.
Sofortiger Shutdown bei stateless und idempotenten Worker-Prozessen
Spring-Ausgangscodeexecutor.shutdownNow();Spring-ZielcodeshutdownCoordinator.drain(deadline);GracefulShutdownRefactoring.javapackage 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;
}
}@Async versteckt Threadwechsel, Fehlerkanal und Executor-Auswahl; Tests werden timingabhängig.
Executor Port / Command
Asynchrone Ausführung über einen benannten Port modellieren und Fehler sowie Completion explizit zurückgeben.
Direkte synchrone Ausführung bei kurzen CPU-Operationen
Spring-Ausgangscode@Async public void sendMail(Command command) { ... }Spring-ZielcodeCompletionStage<Result> result = taskExecutor.submit(command);AsyncExecutorBoundaryRefactoring.javapackage 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); }
}
}Modulgrenzen, zuverlässige Events und versionierte Verträge.
Modulgrenzen, zuverlässige Events und versionierte Verträge.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Direkte Service-Kaskaden koppeln Module und machen Transaktionsfolgen unsichtbar.
Domain Event + Modulgrenze
Fachereignisse im Quellmodul erzeugen und über einen kleinen Event-Port nach erfolgreichem Commit publizieren.
Direkter Aufruf bei streng lokalem, synchronem Verhalten
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeModuleDomainEventRefactoring.class // explizite, testbare BoundaryModuleDomainEventRefactoring.javapackage 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()))); }
}Geschäftstransaktion und Broker-Publish bilden einen unsicheren Dual Write.
Transactional Outbox + Polling Publisher
Ereignis atomar mit Fachdaten speichern; separater Poller übernimmt Zustellung und markiert den Eintrag.
Direkter Publish bei rein best-effort Benachrichtigungen
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeOutboxPollerRefactoring.class // explizite, testbare BoundaryOutboxPollerRefactoring.javapackage 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;
}
}Wiederholte Nachrichten lösen dieselbe Fachaktion mehrfach aus.
Inbox + Idempotent Consumer
Message-ID vor Fachausführung atomar reservieren und Ergebnis beziehungsweise Status persistieren.
Natürlich idempotente Operation ohne Seiteneffekte
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeInboxConsumerRefactoring.class // explizite, testbare BoundaryInboxConsumerRefactoring.javapackage 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;
}
}Globale Retries wiederholen auch fachliche Ablehnungen und vervielfachen Last.
Retry Policy + Error Classification
Fehler in transient, permanent und fachlich klassifizieren; nur transiente Fehler mit Budget wiederholen.
Kein Retry bei synchroner Benutzerkorrektur
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeRetryClassificationRefactoring.class // explizite, testbare BoundaryRetryClassificationRefactoring.javapackage 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));
}
}Retry, Timeout und Circuit Breaker werden ungeordnet und mehrfach verschachtelt.
Pipeline + Policy Object
Resilienzschritte als geordnete, typisierte Policy konfigurieren und zentral validieren.
Ein einzelner Timeout bei sehr einfachem Adapter
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeResiliencePipelineRefactoring.class // explizite, testbare BoundaryResiliencePipelineRefactoring.javapackage 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))); }
}Package-Konventionen sind dokumentiert, aber nicht automatisch geprüft.
Modular Monolith + Fitness Function
Fachmodule mit expliziten APIs schneiden und Abhängigkeiten als ausführbare Regeln prüfen.
ArchUnit-Regeln ohne Spring Modulith
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeSpringModulithBoundaryRefactoring.class // explizite, testbare BoundarySpringModulithBoundaryRefactoring.javapackage 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")); }
}String-basierte Eventnamen und Maps verstecken Schemafehler bis zur Laufzeit.
Typed Contract + Published Language
Events als unveränderliche Records mit Versionsfeld und klarer Semantik modellieren.
Schema Registry mit generierten Klassen
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeTypedEventContractRefactoring.class // explizite, testbare BoundaryTypedEventContractRefactoring.javapackage 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"); }
}
}Breaking Changes werden still in bestehende Endpunkte eingebaut.
API Versioning + Deprecation Policy
Versionen explizit routen, Sunset-Zeitpunkt dokumentieren und Adapter auf gemeinsamen Use Case abbilden.
Additive Evolution ohne neue Version
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeVersionedRestApiRefactoring.class // explizite, testbare BoundaryVersionedRestApiRefactoring.javapackage 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)); }
}AOT, Konfiguration und moderne Spring-Adaptergrenzen.
AOT, Konfiguration und moderne Spring-Adaptergrenzen.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Dynamische Klassenauflösung und breite Reflection verhindern sichere Native-Images.
Reflection Boundary + Explicit Registry
Reflektive Typen an einer Stelle registrieren und Kernlogik auf statische Verträge umstellen.
JVM-only Betrieb ohne AOT-Ziel
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeAotReflectionBoundaryRefactoring.class // explizite, testbare BoundaryAotReflectionBoundaryRefactoring.javapackage 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(); }
}Reflection-, Resource- und Proxy-Hinweise liegen verstreut neben Fachklassen.
Metadata Adapter + Composition Root
AOT-Metadaten zentral aus technischen Adapteranforderungen ableiten.
Keine Runtime Hints bei vollständig statischem Code
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeRuntimeHintsBoundaryRefactoring.class // explizite, testbare BoundaryRuntimeHintsBoundaryRefactoring.javapackage 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) {}
}Viele @Conditional-Varianten machen den aktiven Objektgraphen schwer nachvollziehbar.
Composition Root + Strategy Selection
Adapterauswahl aus typisierter Umgebung zentral zusammensetzen und validieren.
Spring Auto-Configuration für echte Bibliotheken
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeExplicitBeanCompositionRefactoring.class // explizite, testbare BoundaryExplicitBeanCompositionRefactoring.javapackage 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"; }; }
}Starter registrieren unkontrolliert Beans und überschreiben Anwendungspolicies.
Auto-Configuration Boundary + Back-off
Nur technische Defaults liefern, bei eigener Bean sauber zurücktreten und Properties validieren.
Explizite Konfiguration in einer einzelnen Anwendung
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeAutoConfigurationBoundaryRefactoring.class // explizite, testbare BoundaryAutoConfigurationBoundaryRefactoring.javapackage 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; }
}Use Cases lesen Environment-Variablen und Vault-Schlüssel direkt.
Credentials Port + Adapter
Secret-Auflösung in Infrastruktur kapseln und nur kurzlebige typisierte Credentials liefern.
Statische lokale Testkonfiguration
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeCredentialsPortRefactoring.class // explizite, testbare BoundaryCredentialsPortRefactoring.javapackage 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); }
}Jeder Codepfad liest veränderliche Konfiguration einzeln und sieht inkonsistente Werte.
Immutable Snapshot + Atomic Publication
Konfiguration atomar als validierten unveränderlichen Snapshot veröffentlichen.
Neustart bei seltenen Konfigurationsänderungen
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeDynamicConfigurationSnapshotRefactoring.class // explizite, testbare BoundaryDynamicConfigurationSnapshotRefactoring.javapackage 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);} }
}Controller oder Service verwendet framework-spezifischen HTTP-Client direkt.
Port + Declarative Client Adapter
Fachlichen Client-Port definieren und deklaratives Spring-Interface nur im Adapter implementieren.
WebClient-Adapter bei dynamischen Requests
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeHttpInterfaceClientBoundaryRefactoring.class // explizite, testbare BoundaryHttpInterfaceClientBoundaryRefactoring.javapackage 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)); }
}Resolver enthält Autorisierung, N+1-Ladevorgänge und Fachentscheidungen.
Thin Resolver + DataLoader Port
Resolver mappt Request auf Query; Batching und Fachlogik liegen in expliziten Ports beziehungsweise Use Cases.
REST-Endpunkt bei einfacher Ressourcenabfrage
Spring-AusgangscodeframeworkApi.call(); service.save(); eventBus.publish();Spring-ZielcodeGraphqlResolverBoundaryRefactoring.class // explizite, testbare BoundaryGraphqlResolverBoundaryRefactoring.javapackage 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); }
}Reaktive Grenzen, Backpressure, Streaming, AOP, Lifecycle und die finale modulare Komposition.
Reaktive Grenzen, Backpressure, Streaming, AOP, Lifecycle und die finale modulare Komposition.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
WebFlux-Handler enthält Mapping, Fachlogik und technische Fehlerbehandlung in einer reaktiven Kette.
Thin Reactive Adapter + Use Case Port
Handler normalisiert den Request und delegiert an einen frameworkfreien Use Case.
Klassisches MVC bei ausschließlich blockierender Verarbeitung
Spring-Ausgangscodereturn repository.save(request).map(this::toResponse);Spring-ZielcodeReactiveHandlerBoundaryRefactoring.class // explizite, testbare BoundaryReactiveHandlerBoundaryRefactoring.javapackage 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));
}
}Blockierende Datenbank- oder Dateiaufrufe laufen unbemerkt auf Event-Loop-Threads.
Blocking Adapter + Executor Port
Blockierende Arbeit hinter einem expliziten Port auf einen kontrollierten Executor verschieben.
Vollständig nichtblockierender Treiber und reaktive Persistenz
Spring-Ausgangscodereturn eventLoop.submit(() -> legacyStore.load(id));Spring-ZielcodeReactiveBlockingIsolationRefactoring.class // explizite, testbare BoundaryReactiveBlockingIsolationRefactoring.javapackage 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);
}
}Reaktive Transaktion wird über Controller, Repository und Event-Publisher verteilt.
Reactive Unit of Work
Transaktion um einen vollständigen Use Case legen und externe Veröffentlichung nach Commit ausführen.
JDBC-Transaktion bei bewusst blockierendem Modul
Spring-Ausgangscoderepository.save(account); publisher.publish(event);Spring-ZielcodeReactiveTransactionBoundaryRefactoring.class // explizite, testbare BoundaryReactiveTransactionBoundaryRefactoring.javapackage 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));
}
}Unbegrenzte Ereignisströme sammeln Daten im Speicher, bis Latenz und Heap kollabieren.
Backpressure Policy + Bounded Buffer
Kapazität, Overflow-Verhalten und Messgrößen als explizite Policy definieren.
Synchroner Request-Response-Fluss bei geringer Last
Spring-Ausgangscodeevents.buffer().subscribe(this::consume);Spring-ZielcodeReactiveBackpressurePolicyRefactoring.class // explizite, testbare BoundaryReactiveBackpressurePolicyRefactoring.javapackage 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(); }
}SSE-Controller liest direkt Broker-Nachrichten und formatiert Fachereignisse.
Event Stream Port + Presenter
Fachlichen Ereignisstrom typisieren; SSE-Formatierung bleibt im Web-Adapter.
Polling-Endpunkt bei seltenen Aktualisierungen
Spring-Ausgangscodebroker.messages().map(this::toSse);Spring-ZielcodeServerSentEventsBoundaryRefactoring.class // explizite, testbare BoundaryServerSentEventsBoundaryRefactoring.javapackage 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());
}
}Aspekte enthalten Fachentscheidungen, verändern Rückgabewerte oder verstecken wichtige Seiteneffekte.
Decorator + Explicit Interceptor
Nur technische Querschnittsfunktionen als explizite Decorators modellieren; Fachregeln bleiben im Use Case.
Direkter Aufruf bei nur einem einfachen Anwendungsfall
Spring-Ausgangscode@Around("execution(* service..*(..))") Object advice(){ ... }Spring-ZielcodeAopBoundaryRefactoring.class // explizite, testbare BoundaryAopBoundaryRefactoring.javapackage 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); } };
}
}Startup Runner, Scheduler und Listener starten unabhängig und melden Bereitschaft zu früh.
Lifecycle Coordinator + Readiness Gate
Initialisierung, Warm-up, Readiness und Shutdown in einem expliziten Zustandsmodell koordinieren.
Einfacher synchroner Start bei kleiner Anwendung
Spring-Ausgangscode@PostConstruct void startEverything(){ ... }Spring-ZielcodeApplicationLifecycleCoordinatorRefactoring.class // explizite, testbare BoundaryApplicationLifecycleCoordinatorRefactoring.javapackage 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(); }
}
}Komponenten-Scan, Konfiguration und Adapter liegen projektweit verteilt; Modulgrenzen sind nur Konvention.
Composition Root + Package by Feature + Architecture Fitness Function
Pro Feature einen klaren Modulvertrag definieren und die finale Verdrahtung zentral zusammensetzen.
Kleine Anwendung mit einem einzigen fachlichen Modul
Spring-Ausgangscode@ComponentScan("com.company") class App { }Spring-ZielcodeSpringCompositionRootRefactoring.class // explizite, testbare BoundarySpringCompositionRootRefactoring.javapackage 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);
}
}