The sealed result types approach is genuinly clean. Forcing the compiler to handle every case with exhaustive pattern matching eliminates that whole category of runtime surprises where an edge case slips through. I've definetly spent too much time debugging mocked service layers, so the static functions + active record model is refreshing. Only concern is whether the panache pattern scales when you need more sophisticated query building or complex transaction boundaries.
What you typically avoid in this style is mocking repositories or static methods just to force branches. If you need that, the logic probably isn’t separated enough.
So the claim isn’t that mocking disappears.
It’s that mocking stops being the default way to test business rules.
Great article but I can’t get the point why this approach is better. You said it will simplify testing, but given example compare integration test using active record with unit test that mocks repositories. If we use integration tests for both approaches it would be pretty much similar. For me active record hides dependencies - we are pretending that we have pure static methods but data access is still there
That’s a fair critique, and you’re right to call it out.
You’re absolutely correct that this is not pure functional code. The database is still there, and Active Record does hide I/O behind static calls. I’m not claiming referential transparency or side-effect freedom.
The point I’m making is more modest and more practical.
The benefit is not “no integration tests needed”.
It’s where complexity lives and how explicit the domain logic becomes.
In the traditional service + repository style, we usually mix three concerns in one method:
- branching business rules
- data access orchestration
- error handling via exceptions
That combination is what makes unit tests heavy and brittle. You end up mocking repositories not because it’s good design, but because the logic is entangled with infrastructure.
In the data-oriented version, the win is that:
- all business outcomes are explicit data
- branching is enforced by the compiler via sealed types
- control flow is visible, not implicit through exceptions
- tests focus on decisions, not interactions
Yes, you can integration-test both approaches.
And yes, if you only write integration tests, the difference shrinks.
But even in integration tests, this style tends to:
- require less setup
- avoid mock-heavy test pyramids
- make failure cases easier to assert
- surface hidden assumptions earlier
On the “hidden dependencies” point: I agree.
Active Record trades explicit dependencies for local reasoning. That’s a conscious trade-off, not a free lunch.
If I needed strict dependency visibility, cross-cutting concerns, or multiple persistence strategies, I would not use this style.
The article isn’t saying “this replaces layered architecture everywhere”.
It’s saying: for business-rule-heavy flows, modeling outcomes as data and pushing logic into simple operations often reduces accidental complexity.
If nothing else, I think it’s a useful lens to evaluate when a service layer is helping you—and when it’s just habit.
Appreciate you challenging it. That’s exactly the kind of discussion I hoped to trigger.
Belíssimo artigo! Listou os argumentos que eu precisava para conseguir começar a propagar os ganhos econômicos fornecidos pela Programação Orientada a Dados para uma abordagem arquitetural mais enxuta. Muito obrigado.
The sealed result types approach is genuinly clean. Forcing the compiler to handle every case with exhaustive pattern matching eliminates that whole category of runtime surprises where an edge case slips through. I've definetly spent too much time debugging mocked service layers, so the static functions + active record model is refreshing. Only concern is whether the panache pattern scales when you need more sophisticated query building or complex transaction boundaries.
I really like this approach but I don't see how you can avoid mocking.
The test you sketch out:
```
var result = OrderOperations.placeOrder(request);
assertInstanceOf(OutOfStock.class, result);
```
will AFAIK require some mocking of the OrderOperations class or did I miss something?
Good question. The short answer is: I did intentionally simplify.
As written, placeOrder() does touch the database, so you wouldn’t run that exact call as a pure unit test without either:
- a real database (integration-style), or
- refactoring the decision logic.
The key point is what you end up mocking.
In the traditional service + repository model, tests usually mock behavior:
- repository calls
- interaction order
- exception paths
Those tests are tightly coupled to implementation details.
With this approach, you usually do one of two things.
Option 1: Integration-style tests (most common)
No mocks at all.
Use Dev Services or Testcontainers and assert on returned data.
Quarkus is optimized for this style, and it tends to be fast and stable.
Option 2: Split decision logic from persistence
If you want true unit tests, extract the pure decision part:
static OrderResult decide(ProductSnapshot product, OrderRequest request) { … }
That function is fully testable with plain data.
No mocks. No framework. No database.
What you typically avoid in this style is mocking repositories or static methods just to force branches. If you need that, the logic probably isn’t separated enough.
So the claim isn’t that mocking disappears.
It’s that mocking stops being the default way to test business rules.
Great article but I can’t get the point why this approach is better. You said it will simplify testing, but given example compare integration test using active record with unit test that mocks repositories. If we use integration tests for both approaches it would be pretty much similar. For me active record hides dependencies - we are pretending that we have pure static methods but data access is still there
That’s a fair critique, and you’re right to call it out.
You’re absolutely correct that this is not pure functional code. The database is still there, and Active Record does hide I/O behind static calls. I’m not claiming referential transparency or side-effect freedom.
The point I’m making is more modest and more practical.
The benefit is not “no integration tests needed”.
It’s where complexity lives and how explicit the domain logic becomes.
In the traditional service + repository style, we usually mix three concerns in one method:
- branching business rules
- data access orchestration
- error handling via exceptions
That combination is what makes unit tests heavy and brittle. You end up mocking repositories not because it’s good design, but because the logic is entangled with infrastructure.
In the data-oriented version, the win is that:
- all business outcomes are explicit data
- branching is enforced by the compiler via sealed types
- control flow is visible, not implicit through exceptions
- tests focus on decisions, not interactions
Yes, you can integration-test both approaches.
And yes, if you only write integration tests, the difference shrinks.
But even in integration tests, this style tends to:
- require less setup
- avoid mock-heavy test pyramids
- make failure cases easier to assert
- surface hidden assumptions earlier
On the “hidden dependencies” point: I agree.
Active Record trades explicit dependencies for local reasoning. That’s a conscious trade-off, not a free lunch.
If I needed strict dependency visibility, cross-cutting concerns, or multiple persistence strategies, I would not use this style.
The article isn’t saying “this replaces layered architecture everywhere”.
It’s saying: for business-rule-heavy flows, modeling outcomes as data and pushing logic into simple operations often reduces accidental complexity.
If nothing else, I think it’s a useful lens to evaluate when a service layer is helping you—and when it’s just habit.
Appreciate you challenging it. That’s exactly the kind of discussion I hoped to trigger.
Belíssimo artigo! Listou os argumentos que eu precisava para conseguir começar a propagar os ganhos econômicos fornecidos pela Programação Orientada a Dados para uma abordagem arquitetural mais enxuta. Muito obrigado.
You’re welcome!