Optivem Journal

Optivem Journal

Hexagonal Architecture

Hexagonal Architecture: Why I Don't Abstract the Database for Swappability

Misconception: “Repository interfaces exist so databases can be swapped”

Valentina Jemuović's avatar
Valentina Jemuović
Jun 12, 2026
∙ Paid

You read the book. You applied Clean Architecture in real code. And it went downhill.

That’s not on you — it’s the gap between the book and reality. ORM entities sneaking into the domain, business logic forced into memory until the system crawls, external DTOs leaking straight into your core. I see these three mistakes in almost every codebase that “did Clean Architecture.”

📅 Join me: Clean Architecture: Stop doing it wrong on Wed 26th Aug, 5:00 - 6:30 PM (CEST)

→ Reserve your spot


🔒 Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders & Senior Software Developers apply TDD in Legacy Code.


Misconception: “Repository interfaces exist so databases can be swapped”

One of the most common criticisms of Hexagonal Architecture and Clean Architecture goes like this:

“Why are you abstracting over the database? You’re never going to swap it anyway.”

And honestly?

If the goal is database swappability, I partly agree.

Most teams aren’t going to wake up tomorrow and replace PostgreSQL with MongoDB.

Most applications will use the same database for years.

So if database swappability is your main justification for repository interfaces, it’s not a particularly convincing one.

But that’s not why I use them.

❌ Dragging the database into every change and every test

You want to test:

Premium customers receive 20% discount.
Regular customers receive 5% discount.

That sounds simple.

But if the business logic directly calls ORM classes / uses Entity Framework or JPA, the test looks like this:

1. Insert test customer
2. Execute business logic
3. Read result
4. Clean database

@SpringBootTest
@Testcontainers
class DiscountServiceDatabaseTest {

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

    @DynamicPropertySource
    static void datasource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired CustomerRepository customers;       // JPA repository
    @Autowired DiscountService discountService;    // business logic, talks to JPA directly

    @AfterEach
    void cleanUp() {
        customers.deleteAll();                     // 4. clean database
    }

    @Test
    void premiumCustomersReceive20PercentDiscount() {
        // 1. insert test customer
        var alice = customers.save(new CustomerEntity("Alice", Tier.PREMIUM));

        // 2. execute business logic (which loads the customer back out of the DB)
        var finalPrice = discountService.priceFor(alice.getId(), BigDecimal.valueOf(100));

        // 3. read result
        assertThat(finalPrice).isEqualTo(BigDecimal.valueOf(80));
    }

    @Test
    void regularCustomersReceive5PercentDiscount() {
        // 1. insert test customer
        var bob = customers.save(new CustomerEntity("Bob", Tier.REGULAR));

        // 2. execute business logic (which loads the customer back out of the DB)
        var finalPrice = discountService.priceFor(bob.getId(), BigDecimal.valueOf(100));

        // 3. read result
        assertThat(finalPrice).isEqualTo(BigDecimal.valueOf(95));
    }
}

Just to verify a discount calculation.

The database isn’t the thing you’re interested in.

The business rule is.

But you’re forced to involve the database just to test it.

✅ Repository interfaces exist so business logic can be tested independently from infrastructure

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 Valentina Jemuović, Optivem · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture