IT-Jobmarkt 2026: Welche Fähigkeiten Entwickler lernen sollten

1. Übersicht

Der IT-Jobmarkt ändert sich in eine Richtung, die von jedem Entwickler mehr verlangt. Eine Funktion schreiben zu können, reicht nicht aus. Du musst auch verstehen, wie diese Funktion in der Production läuft, welche Fehler sie erzeugt und wie viel sie kostet.

Dieser Artikel enthält keine Zahlenmaterial oder Lohnprognosen. Stattdessen analysiert er fünf Grundfähigkeitsgruppen, die derzeit beachtet werden, erläutert die Gründe und veranschaulicht jede Gruppe mit Java 21- / Spring Boot 3.x-Code. Nach dem Lesen wirst du wissen, welche Fähigkeiten Priorität haben, wie du sie sofort auf dein Projekt anwendest und wann du keinem Trend folgen solltest.

2. Warum sich die Anforderungen an Fähigkeiten verschieben

Es gibt drei Haupttreiber, und du kannst sie selbst in den meisten Jobangeboten beobachten:

  • KI-Tools zur Code-Generierung sind weit verbreitet. “Boilerplate”-Code wird schneller erzeugt. Der Wert von Entwicklern verschiebt sich zu Design, Review, Testing und Verantwortung für Ergebnisse.
  • Unternehmen fokussieren auf Betriebskosten. Ein Service, der weniger Ressourcen verbraucht und einfacher zu diagnostizieren ist, spart direkt Geld.
  • Kleinere Teams, breiterer Ownership. Das Modell “you build it, you run it” zwingt Entwickler, Packaging, Monitoring und Troubleshooting zu verstehen.

Daraus ergeben sich fünf Fähigkeitsgruppen als lohnende langfristige Investitionen.

3. KI-Integration mit Kontrolle in Anwendungen

Die am häufigsten erwähnte Fähigkeit ist nicht “ChatBot bedienen können”. Es ist die Fähigkeit, LLMs in echte Systeme zu integrieren, während man Stabilität, Testbarkeit und Fallback-Pläne bewahrt.

Das Kernprinzip: Betrachte das LLM als eine externe, nicht vertrauenswürdige Abhängigkeit. Seine Ausgabe kann falsch formatiert, langsam oder fehlerhaft sein.

3.1. LLM hinter einer Interface abstrahieren

@FunctionalInterface
public interface LlmClient {
    String complete(String prompt);
}

public enum TicketCategory { BILLING, TECHNICAL, OTHER }

public class TicketClassifier {

    private final LlmClient llm;

    public TicketClassifier(LlmClient llm) {
        this.llm = llm;
    }

    public TicketCategory classify(String message) {
        String prompt = """
            Klassifiziere die folgende Support-Anfrage in genau ein Label: BILLING, TECHNICAL, OTHER.
            Gib nur das Label zurück, keine Erklärung.
            Anfrage: %s
            """.formatted(message);
        try {
            String raw = llm.complete(prompt).trim().toUpperCase();
            return TicketCategory.valueOf(raw);
        } catch (RuntimeException e) {
            return TicketCategory.OTHER;
        }
    }
}

Erklärung:

  • LlmClient ist deine eigene Interface. Die echte Implementierung kann einen beliebigen Anbieter über HTTP aufrufen oder eine Bibliothek wie Spring AI nutzen. Die Business-Logik hängt nicht vom konkreten Anbieter ab.
  • Der Prompt fordert ein einzelnes Label. Das Format-Constraint hilft dir, das Ergebnis zu parsen.
  • TicketCategory.valueOf(raw) parsed und validiert gleichzeitig. Wenn das Modell einen langen Satz zurückgibt, wird IllegalArgumentException abgefangen und das System fällt auf OTHER zurück. Netzwerkfehler (meist RuntimeException) fallen auch in diesen Branch.

3.2. Test ohne echtes Modell

Weil LlmClient eine functional interface ist, kannst du ein Lambda als Fake übergeben:

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;

class TicketClassifierTest {

    @Test
    void givenValidLabel_whenClassify_thenReturnCategory() {
        TicketClassifier classifier = new TicketClassifier(prompt -> " billing\n");

        assertThat(classifier.classify("Mir wurde zweimal Geld abgebucht"))
            .isEqualTo(TicketCategory.BILLING);
    }

    @Test
    void givenUnexpectedOutput_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> "Das könnte BILLING sein");

        assertThat(classifier.classify("Mir wurde zweimal Geld abgebucht"))
            .isEqualTo(TicketCategory.OTHER);
    }

    @Test
    void givenClientFailure_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> {
            throw new IllegalStateException("timeout");
        });

        assertThat(classifier.classify("App stürzt ab")).isEqualTo(TicketCategory.OTHER);
    }
}

Der erste Test zeigt, dass dein Code mit Whitespace und Kleinbuchstaben umgehen kann. Die beiden anderen Tests beweisen, dass das System nicht zusammenbricht, wenn das Modell “viel redet” oder die Verbindung fehlschlägt. Genau das wollen Arbeitgeber sehen: Du weißt, dass LLMs fehlschlagen, und hast dafür geplant.

Hinweis: Nutze keine LLMs für Aufgaben, bei denen eine Rule, ein Regex oder eine SQL-Abfrage korrekt und billiger sind. Auch nicht für Entscheidungen, die ein feststehendes Ergebnis und Audit-Trail brauchen, wie Abrechnungen oder Berechtigungen.

4. Modernes Concurrency mit Virtual Threads

Der Großteil von Backends ist I/O-bound: Datenbankaufrufe, API-Aufrufe. Java 21 macht Virtual Threads zu einem offiziellen Feature, mit dem du blocking Code schreiben kannst, wie du es gewohnt bist, aber trotzdem sehr viele gleichzeitige Requests bedienst. Zu wissen, wann man sie nutzt und wann nicht, ist ein klarer Vorteil in Interviews.

import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class PriceAggregator {

    public static void main(String[] args) throws Exception {
        List<String> suppliers = List.of("SupplierA", "SupplierB", "SupplierC");
        long start = System.currentTimeMillis();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = suppliers.stream()
                .map(s -> executor.submit(() -> fetchPrice(s)))
                .toList();
            for (Future<String> f : futures) {
                System.out.println(f.get());
            }
        }
        System.out.println("Gesamtzeit: " + (System.currentTimeMillis() - start) + " ms");
    }

    static String fetchPrice(String supplier) throws InterruptedException {
        Thread.sleep(Duration.ofSeconds(1)); // simuliert langsamen API-Aufruf
        return supplier + " -> 100000 VND, virtual=" + Thread.currentThread().isVirtual();
    }
}
  • newVirtualThreadPerTaskExecutor() erstellt einen Virtual Thread pro Task. Du musst die Pool-Größe nicht abstimmen.
  • Der try-with-resources-Block stellt sicher, dass der Executor auf alle Tasks wartet, bevor er sich schließt.
  • Drei einsekündige Aufrufe laufen parallel, also dauert die Gesamtzeit ungefähr eine Sekunde:
SupplierA -> 100000 VND, virtual=true
SupplierB -> 100000 VND, virtual=true
SupplierC -> 100000 VND, virtual=true
Gesamtzeit: 1015 ms

In Spring Boot 3.2+ aktivierst du Virtual Threads für Request-Handling mit einer Zeile:

spring.threads.virtual.enabled=true

Hinweis: Virtual Threads machen CPU-bound Code nicht schneller. Pass auf große synchronized-Blöcke auf, die I/O-Operationen umgeben, denn sie können einen Virtual Thread an einen Carrier Thread binden. Die genaue Auswirkung hängt vom JDK-Release ab. Außerdem können mehr gleichzeitige Tasks den Connection Pool deiner Datenbank leeren. Du musst Ressourcen auf der Backend-Seite immer noch begrenzen.

5. Observability: Verstehe, was dein System tut

Wenn du auch für den Betrieb verantwortlich bist, musst du “Warum ist das langsam?” mit Daten beantworten können. Metrics, Logs und Traces sind die drei Säulen. Im Spring-Ökosystem sind Micrometer und Actuator der natürliche Startpunkt.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Füge eine selbst definierte Business-Metrik hinzu:

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final Counter placedOrders;

    public OrderService(MeterRegistry registry) {
        this.placedOrders = Counter.builder("orders.placed")
            .description("Anzahl erfolgreich aufgegebener Bestellungen")
            .tag("channel", "web")
            .register(registry);
    }

    public void placeOrder(String orderId) {
        // ... Bestellung speichern, PaymentService aufrufen ...
        placedOrders.increment();
    }
}
management.endpoints.web.exposure.include=health,metrics
  • MeterRegistry wird von Spring Boot automatisch konfiguriert und in den Bean injiziert.
  • Der Tag channel ermöglicht es, Metriken nach Vertriebskanal zu trennen, wenn du ein Dashboard ansiehst.
  • Die Config-Zeile exponiert nur zwei Endpoints: health und metrics, nicht alles.

Nach ein paar Bestellungen, rufe GET /actuator/metrics/orders.placed auf:

{
  "name": "orders.placed",
  "description": "Anzahl erfolgreich aufgegebener Bestellungen",
  "measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
  "availableTags": [ { "tag": "channel", "values": [ "web" ] } ]
}

Hinweis: Verwende keine unbegrenzte, hochkardinale Werte (wie orderId oder customerId) als Tags. Jeder Tag-Wert erzeugt eine neue Time Series, also bläht sich dein Metrics-System schnell auf. Exponiere auch nicht alle Actuator-Endpoints ins Internet.

6. Testing mit echter Infrastruktur via Testcontainers

Wenn KI Code schnell generiert, ist Testing der Beweis, dass Code korrekt ist. Integration Tests auf echter Datenbank, nicht auf einer in-memory Fake mit anderem Dialect, fangen Fehler, die Mocks verpassen.

Diese Dependencies werden von Spring Boot versioniert:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>

Minimalistische Entity und Repository:

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;
    private String email;

    protected Customer() {}
    public Customer(String email) { this.email = email; }
    public Long getId() { return id; }
    public String getEmail() { return email; }
}

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findByEmail(String email);
}

Integration Test:

@SpringBootTest
@Testcontainers
class CustomerRepositoryIT {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired
    CustomerRepository repository;

    @Test
    void givenSavedCustomer_whenFindByEmail_thenReturnCustomer() {
        repository.save(new Customer("lan@example.com"));

        assertThat(repository.findByEmail("lan@example.com"))
            .isPresent()
            .get()
            .extracting(Customer::getEmail)
            .isEqualTo("lan@example.com");
    }
}
  • @Testcontainers und @Container starten eine echte PostgreSQL in Docker, bevor Tests laufen.
  • @ServiceConnection (ab Spring Boot 3.1) injiziert URL, Username und Password des Containers automatisch in DataSource. Du brauchst keine @DynamicPropertySource.
  • Die Query findByEmail, die Spring Data erzeugt, läuft auf genau demselben Datenbank-Typ wie in Production.

Hinweis: Testcontainers brauchen Docker auf deinem Dev-Rechner und in der CI. Tests sind auch langsamer als Unit Tests. Halte den Großteil der Logik in reinen Unit Tests, nutze Testcontainers nur für die Integrations-Schicht wie Repository, Messaging und Migrations.

7. Container verpacken und betreiben

Zu verstehen, wie die Anwendung verpackt und ausgeführt wird, ist Mindestanforderung für Kubernetes oder Cloud-Plattformen. Ein schlankes Multi-Stage Dockerfile zeigt, dass du auf Größe und Sicherheit des Image achtest:

FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
USER 1000
ENTRYPOINT ["java", "-jar", "app.jar"]
  • Stage build hat JDK und Maven Wrapper zum Kompilieren.
  • Die Final-Stage hat nur JRE und das jar-File, also kleineres Image und weniger Angriffsfläche.
  • USER 1000 verhindert, dass der Container als root läuft – ein sehr häufiger Fehler.

Du kannst das /actuator/health-Endpoint aus Abschnitt 5 als liveness/readiness probe beim Deployment nutzen.

Hinweis: Falls dein Team bereits Buildpacks nutzt (z. B. über ./mvnw spring-boot:build-image), brauchst du kein eigenes Dockerfile. Wichtig ist, zu verstehen, was im Image enthalten ist, welcher User es ausführt und wie Speicherbeschränkung die JVM beeinflusst.

8. Wann man keinem Trend folgen sollte

Fähigkeiten erfordern auch Nüchternheit:

  • Vernachlässige die Grundlagen nicht. Datenstrukturen, SQL, HTTP, Transaktionen, Fehlerbehandlung werden in jedem Interview abgefragt. KI und Frameworks ändern schneller als dieses Wissen.
  • Lerne nicht planlos. Wähle ein bis zwei Fähigkeiten, die zu deinem aktuellen Job passen, wende sie auf ein echtes Projekt an, dann erweitere. Eine PR, die Metrics und Integration Tests hinzufügt, ist wertvoller als zehn ungenutzte Zertifikate.
  • Führe Technologie nicht ein, nur um es im CV zu haben. Ein kleines System mit wenigen Nutzern braucht kein Kubernetes, keine Microservices oder LLMs. Zu sagen “wir brauchen das nicht” mit guter Begründung ist auch eine hochgeschätzte Fähigkeit.
  • Vertrau nicht blind auf Code-Generierungs-Tools. Du trägst die Verantwortung. Lies, verstehe und schreibe Tests für jeden erzeugten Code.

9. Fazit

Der IT-Markt belohnt Entwickler, die den ganzen Lebenszyklus einer Funktion beherrschen: Design, Code, Testing, Verpackung und Betrieb. Dieser Artikel hat fünf lohnende Fähigkeitsgruppen durchgegangen:

  • LLMs hinter eine Interface mit Validierung und Fallback integrieren.
  • Virtual Threads für I/O-bound Workloads nutzen.
  • Observability durch Micrometer und Actuator hinzufügen.
  • Integration Tests auf echter Infrastruktur mit Testcontainers schreiben.
  • Container sicher verpacken.

Jede Fähigkeit hat ihre Grenzen, und diese zu kennen ist genauso wichtig, wie sie nutzen zu können. Wähl ein Thema, wende es diese Woche auf deinen aktuellen Service an. Das ist der schnellste Weg, Trends in echte Erfahrung zu verwandeln.

karrieretech-trendsjavaspring-bootai