Wiederverwendbare Spring Data Repositories mit SpEL und @NoRepositoryBean

Das DRY-Prinzip ("Don't Repeat Yourself") gehört zu den Grundpfeilern wartbarer Softwarearchitektur. In der Praxis wird dieses Prinzip jedoch häufig durchbrochen – insbesondere beim Einsatz von Spring Data Repositories. Wiederkehrende Datenzugriffslogik wie findByStatus oder markAsProcessed taucht oft mehrfach auf, nur leicht variiert für unterschiedliche Entitäten. Ein typisches Beispiel dafür bietet die Implementierung des Outbox-Patterns. Solche Wiederholungen führen schnell zu redundanter, schwer wartbarer Codebasis. Dieser Artikel zeigt, wie sich durch den gezielten Einsatz von @NoRepositoryBean und Spring Expression Language (SpEL) wiederverwendbare Repository-Verträge realisieren lassen – elegant, robust und ganz im Sinne von DRY.

Bei dem gennantem Beispiel für das Outbox Pattern, sollten in unserem Fall mehrere Outbox Tabellen für unterschiedliche Event-Typen angelegt werden (zum Beispiel OrderOutbox oder PaymentOutbox). Natürlich ist die Struktur und auch die Funktonalität für jede dieser Outbox-en exakt gliech. Wir wollten hier eine Lösung implementieren, die:

Lösung mit @NoRepositoryBean und SpEL (Spring Expression Language)

Was es braucht, ist ein abstraktes Repository, das nicht an eine bestimmte Entität gebunden ist – und von dem es auch keine Instanzen in der Anwendung geben muss. Spring Data JPA stellt dafür mit @NoRepositoryBean eine einfache Möglichkeit bereit, um generische Basis-Interfaces zu definieren. In Kombination mit Generics und der Spring Expression Language (SpEL) entsteht so eine elegante und wiederverwendbare Lösung für gemeinsam genutzte Datenzugriffslogik.

Zur Veranschaulichung dient im Folgenden eine abstrahierte Outbox-Klasse, wie sie typischerweise beim Outbox-Pattern zum Einsatz kommt:

@Getter @Setter
@MappedSuperclass
public abstract class AbstractOutbox {
    
    private boolean processed;
    private String aggregateId;
    private String eventType;
    private String payload;
    private LocalDateTime createdAt;
    
    public abstract Long getId();

    // weitere Properties
}

Umsetzung konkreter Outbox-Entitäten:

@Entity
@Table(name = "order_outbox")
public class OrderOutbox extends AbstractOutbox {
    // Optional: Felder spezifisch für Bestellungen
}

@Entity
@Table(name = "payment_outbox")
public class PaymentOutbox extends AbstractOutbox {
    // Optional: Felder spezifisch für Zahlungen
}

Grundstruktur des generischen Basis-Repository-Interfaces:

@NoRepositoryBean
public interface AbstractOutboxRepository<T extends AbstractOutbox> 
    extends JpaRepository<T, Long> {
    
    @Query("select o from #{#entityName} o where o.processed = false order by o.createdAt asc")
    List<T> findUnprocessed();

    @Modifying
    @Query("update #{#entityName} o set o.processed = true where o.id = :id")
    void markAsProcessed(@Param("id") Long id);

    // Weitere gemeinsame Aufgaben für Outbox...
}

Die Annotation @NoRepositoryBean verhindert, dass Spring eine konkrete Implementierung dieses abstrakten Repositories generiert. Durch den Einsatz von SpEL #{#entityName} wird der jeweilige Entitätsname zur Laufzeit korrekt aufgelöst – ideal für generische JPQL-Abfragen. Die Verwendung von Generics stellt dabei sicher, dass die Wiederverwendung typensicher für die jeweilige Outbox implementiert werden kann.

Auf Basis des generischen Interfaces lassen sich konkrete Outbox-Repositories für unterschiedliche Entitäten mit minimalem Aufwand ableiten:

public interface OrderOutboxRepository extends AbstractOutboxRepository<OrderOutbox> {
    // Spezielle Abfragen für Bestellungen hier
}

public interface PaymentOutboxRepository extends AbstractOutboxRepository<PaymentOutbox> {
    // Spezielle Abfragen für Zahlungen hier
}

Durch die Anwendung dieser Strategien wird Copy&Paste-Code vermieden und gleichzeitig eine saubere, erweiterbare Struktur für zukünftige Implementierungen geschaffen. Ein zusätzlicher Vorteil: Da die gemeinsame Logik zentral im generischen Repository liegt, genügt es, sie einmal in Unit-Tests abzudecken – die Testabdeckung gilt dann automatisch für alle spezialisierten Outbox-Repositories.