Przewartościowanie OOP z lat 90. w Javie: DOP, Scoped Values i Loom w 2026 roku
Pragmatyczne spojrzenie na to, jak Data-Oriented Programming i nowoczesne prymitywy JDK redukują podatek od abstrakcji w systemach zamkniętych.
Przez dekady wzorce projektowe z grupy Gang of Four (GoF) stanowiły złoty standard programowania obiektowego. Jeśli miałeś logikę warunkową, budowałeś Factory. Jeśli algorytmy miały być wymienne, stosowałeś Strategy.
Choć wzorce te pozostają absolutnym fundamentem, ich ślepe stosowanie we współczesnej Javie (wersje 21 do 26) często wprowadza całkowicie niepotrzebny Podatek od Abstrakcji (Abstraction Tax).
Kiedy projektowałem Exeris Kernel, przyświecał mi cel “No Waste Compute”. Nie oznaczało to ślepego pościgu za mikro-optymalizacjami, ale raczej fundamentalne przemyślenie przepływu sterowania, propagacji kontekstu i współbieżności przy użyciu nowoczesnych prymitywów JVM.
Oto pragmatyczne spojrzenie na to, w jakim kierunku zmierza JVM i jak Data-Oriented Programming (DOP) w połączeniu z Project Loom zmienia architektoniczne zasady gry dla systemów zamkniętych (closed-domain).
Zderzenie z rzeczywistością Enterprise (Ścieżka migracji)
Zanim przejdziemy do kodu, zejdźmy na ziemię.
1. Ścieżka migracji: Systemów nie przepisuje się z dnia na dzień.
- Java 21 LTS daje Ci Rekordy, Sealed Interfaces, Pattern Matching i stabilne Wątki Wirtualne (GA). Możesz wdrożyć DOP już dziś.
- Java 25 wprowadza
ScopedValuejako funkcję stabilną (GA), naprawiając problem propagacji kontekstu. - Java 26+ kontynuuje stabilizację
StructuredTaskScope(STS). Zastrzeżenie: W JDK 26 mechanizm ten znajduje się w szóstej fazie preview (JEP 525). Uruchomienie go na produkcji wymaga flagi--enable-previewi zgody biznesu. Jest wysoce stabilny, ale jeszcze nie jest to ostateczny standard.
2. Ograniczenie “Closed World”: Przedstawione poniżej podejście DOP jest zaprojektowane dla domen zamkniętych (np. główna logika biznesowa konkretnego mikroserwisu). Jeśli budujesz system otwarty (architektura pluginów, rozszerzalny framework lub SPI), musisz przestrzegać zasady Open/Closed Principle. W takich przypadkach interfejsy sealed są złym narzędziem, a tradycyjny polimorfizm i Service Registries pozostają właściwym wyborem.
Krok 1: Podejście klasyczne (Zrozumieć podatek)
Historycznie, aby przetwarzać różne metody płatności, polegaliśmy na polimorficznych klasach Strategy zarządzanych przez Factory. Jeśli musieliśmy przekazać kontekst (np. Transaction ID) w głąb stosu wywołań, używaliśmy ThreadLocal.
public class PaymentService {
public void process(String type) {
PaymentStrategy strategy = PaymentFactory.get(type);
strategy.process();
}
}
Postawmy sprawę jasno: alokacja pojedynczego 24-bajtowego obiektu Strategy na żądanie nie zabije Twojej sterty (heap). Prawdziwy “Podatek od Abstrakcji” w dużej skali wynika z kumulacji narzutu pośredniości (indirection): obciążenia kognitywnego, kosztów polimorficznego dispatchu na ekstremalnych hot-pathach, głębokiej złożoności grafu obiektów, a co najważniejsze – gigantycznego narzutu pamięciowego wynikającego z kopiowania map InheritableThreadLocal przy tworzeniu tysięcy wątków wirtualnych.
Krok 2: Data-Oriented Programming
W nowoczesnej Javie możemy oddzielić dane od zachowania. Zamiast polimorficznej Fabryki zwracającej obiekty typu Strategy, używamy Sealed Interfaces (zapapieczętowanych interfejsów) i Rekordów do modelowania naszej domeny, oraz Pattern Matchingu do kierowania logiką (dispatch).
W DOP rekordy muszą gwarantować poprawność swojego stanu w momencie utworzenia, wykorzystując Konstruktory Kompaktowe (Compact Constructors).
import java.math.BigDecimal;
// 1. Zamknięta hierarchia. Kompilator gwarantuje wyczerpywalność (exhaustiveness).
public sealed interface PaymentMethod permits CreditCard, Blik {}
// 2. Konstruktory kompaktowe zapobiegają nieprawidłowemu stanowi
public record CreditCard(String cardNumber) implements PaymentMethod {
public CreditCard {
if (cardNumber == null || !cardNumber.matches("\\d{16}")) {
throw new IllegalArgumentException("Invalid card format");
}
}
public String getLastFourDigits() { return cardNumber.substring(12); }
}
public record Blik(String code) implements PaymentMethod {
public Blik {
if (code == null || !code.matches("\\d{6}")) {
throw new IllegalArgumentException("Invalid BLIK code");
}
}
}
W przypadku logiki zamkniętej (closed-domain), całkowicie eliminuje to potrzebę stosowania Fabryki. Zyskujesz wyczerpywalność sprawdzaną na etapie kompilacji, gwarantowaną poprawność danych i znacznie bardziej przewidywalny przepływ sterowania (control flow).
Zmiana w modelu pamięci

Krok 3: Scoped Values (Kontekst bez wycieków)
Jeśli zagnieżdżona logika wymaga ID Transakcji do logowania, InheritableThreadLocal staje się potężnym wąskim gardłem. Kopiowanie stanu pomiędzy milionami wątków wirtualnych niszczy całą “lekkość” Project Loom.
W JDK 25, Scoped Values są stabilne (GA). ScopedValue zapewnia niemutowalny, jednokierunkowy (z góry na dół) przepływ danych. Drastycznie redukuje to narzut na dziedziczenie i automatycznie znika, gdy zakres wykonywania (scope) się kończy.
public class PaymentContext {
public static final ScopedValue<String> TX_ID = ScopedValue.newInstance();
}
// Bindowanie kontekstu
ScopedValue.where(PaymentContext.TX_ID, "TX-9981").run(() -> {
System.out.println("Processing ID: " + PaymentContext.TX_ID.get());
});
(Uwaga: Ponieważ ScopedValues działają wyłącznie w dół stosu (downward-only), wzorce takie jak aktualizacja kontekstu MDC głęboko w drzewie wywołań wymagają odpowiednich dostosowań architektonicznych).
Krok 4: Wykonanie (Czyste DOP + Structured Concurrency)
Naprawiliśmy modelowanie danych (DOP) i propagację stanu (ScopedValue). Teraz musimy zająć się wykonaniem (execution).
Jeśli realizujemy płatność, wywołując jednocześnie serwis antyfraudowy, i serwis fraudowy zgłosi błąd, płatność musi zostać natychmiast przerwana (Fail-Fast).
W 2026 roku wielu programistów nadal używa do tego bibliotek takich jak Resilience4j. Wyjaśnijmy to jasno: StructuredTaskScope nie zastępuje mechanizmów takich jak retry (ponowienia) czy circuit breakers (które powinny znajdować się w warstwie Service Mesh / Envoy). Jednakże, STS natywnie zastępuje aplikacyjne bulkheads (izolacje) i timeouts (limity czasu).
Oto implementacja przy użyciu API w wersji Preview z JDK 26. Rozwidlamy (fork) wątki wirtualne i przekazujemy zwalidowane rekordy danych bezpośrednio do funkcji.
import java.math.BigDecimal;
import java.util.UUID;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public record PaymentRequest(BigDecimal amount, PaymentMethod method) {}
public class PaymentOrchestrator {
public void handle(PaymentRequest request) {
String txId = UUID.randomUUID().toString().substring(0, 8);
ScopedValue.where(PaymentContext.TX_ID, txId).run(() -> {
executeConcurrentWorkflow(request);
});
}
private void executeConcurrentWorkflow(PaymentRequest request) {
// Wymusza rygorystyczny model współbieżności z ograniczeniami własności (ownership)
try (var scope = StructuredTaskScope.open()) {
Subtask<String> paymentTask = scope.fork(() -> executePayment(request.method(), request.amount()));
Subtask<Boolean> fraudTask = scope.fork(() -> performFraudCheck(request));
// Automatycznie przerywa (interrupt) pozostałe zadania w grupie, jeśli jedno z nich zakończy się błędem
scope.join();
System.out.println("Payment: " + paymentTask.get());
System.out.println("Fraud Check: " + (fraudTask.get() ? "Clear" : "Suspicious"));
} catch (StructuredTaskScope.FailedException e) {
System.err.println("Transaction aborted: " + e.getCause().getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private String executePayment(PaymentMethod method, BigDecimal amount) throws InterruptedException {
System.out.println("[Tx: " + PaymentContext.TX_ID.get() + "] Executing...");
Thread.sleep(500); // Symulacja I/O
return switch (method) {
case CreditCard c -> "Processing card ending in " + c.getLastFourDigits();
case Blik b -> "Authorizing BLIK code: " + b.code();
};
}
private boolean performFraudCheck(PaymentRequest request) throws InterruptedException {
Thread.sleep(300);
return true;
}
}
Natywna Architektura Fail-Fast

Pragmatyczny Werdykt
To, co zastępuje tradycyjne wzorce GoF w systemach zamkniętych (closed-domain), nie jest nowym wzorcem, ale zmianą w natywnych prymitywach JVM: dane (Rekordy), kontekst (ScopedValue) i wykonanie (StructuredTaskScope). Łącząc te prymitywy, osiągamy:
- Zredukowaną pośredniość (Reduced Indirection): Przekazujemy rygorystycznie zwalidowane rekordy do funkcji uruchomionych na wątkach wirtualnych, omijając warstwy proxy i poprawiając przewidywalność.
- Bezpieczeństwo danych: Konstruktory Kompaktowe gwarantują, że nieprawidłowy stan nigdy nie wejdzie do potoku przetwarzania.
- Natywny Fail-Fast:
StructuredTaskScopenatywnie obsługuje propagację anulowania zadania pomiędzy granicami wątków.
Projektowanie systemów w 2026 roku nie polega na całkowitym usunięciu OOP czy gonieniu za mitami o zerowej alokacji. Polega na zrozumieniu, które problemy infrastrukturalne są obecnie natywnie rozwiązywane przez JVM oraz Twój Service Mesh — i na pragmatycznym usuwaniu obejść (workarounds), na których polegaliśmy w przeszłości.
Jeśli chcesz zobaczyć, jak ten model skaluje się poza prostą obsługę żądań i ewoluuje w trwałe (durable) środowisko uruchomieniowe off-heap, sprawdź repozytorium Exeris Kernel na GitHubie.