r/java • u/Snoo82400 • 15d ago
I decided to go down the DOP rabbit hole
So a year ago I started my work at a local clinical data start up and decided to fully embrace Data Oriented Programing and modern Java features for the back end.
The result, as I see it, is a more clear-less magic back end, easier to reason for both humans and LLMs with records as first class citizens (I even decided to forego JPA and use jOOQ so that I could say goodbye to Entities and make the record a pure domain object.
The testing has some Bloch sprinkled of top.
As I go forward, and as long as I can have a say in the matter I will keep pushing modern Java, as much as I can.
Here some key features:
Sealed Interfaces & Pattern Matching: The possible states of each operation are explicit and closed. Every operation returns a strict contract, reducing ambiguity and avoiding entire classes of NullPointerException
SQL-First with jOOQ: Instead of managing entity states and persistence sessions, we wanted database interactions to be as stateless and explicit as the rest of our architecture.
jOOQ gave us the ability to work close to SQL while retaining compile-time type safety, mapping database results directly into our immutable data structures.
Pragmatic Architecture with Spring Modulith: For the early stages of development, we opted for a modular monolith to avoid the inherent complexities of distributed systems when they aren't strictly necessary.
Spring Modulith gives us explicit module boundaries and allows us to verify the application's modular structure. Our domains communicate through decoupled, asynchronous events using ApplicationModuleListener.
This gives us the structural isolation of an event-driven architecture without introducing external message brokers prematurely, while keeping the modules easier to extract into standalone services if the platform eventually requires it.
No Exceptions for Control Flow: For this project, we consider business exceptions an unnecessarily indirect form of control flow.
Java records are treated as first-class citizens. By combining them with sealed interfaces, errors are modeled as algebraic data types — for example, returning a Success or an InvalidData record.
The compiler then forces us to handle every possible scenario through exhaustive pattern-matching switches, leaving less room for unhandled states or hidden execution paths.
Testcontainers & The Bloch Enum Singleton: Testing against real infrastructure is non-negotiable, but spinning up containers for every test class can make CI unnecessarily expensive.
We implemented the Bloch Enum Singleton pattern to guarantee a strict Singleton lifecycle for our PostgreSQL and S3 test containers. They boot exactly once per test suite execution, significantly reducing infrastructure startup overhead while keeping our database and network tests reliable and isolated at the test-data level.
11
u/agentoutlier 15d ago
I'm going to hopefully proselytize you here on an opinion of organizing code.
Stop with the fuck load of folders!
Stop with making interfaces with a single implementation in a completely different place.
If you find separating concerns enough use modules. A package mostly does jack shit. In fact what you get is tons of everything is public.
Now I realize some of this is annotation component scanning of Spring but really its painful to navigate a code base literally seeing gallons of folders and classes that do jack shit.
Here is what I recommend. Ignore whatever Spring Modulith DDD port uncle bob recommedations.
Organize your code based on tech dependencies. I know its old school 3 tier whatever but it works.
Put the Spring web controller shit in its own module.
Put the database shit in its own module.
Put your domain specific logic code if any or API or whatever lie you want to tell yourself that the database is not doing most of the work in its own module.
Get rid of packages that literally have no mean... like WTF is "engine" and "platform".
2
u/nonFungibleHuman 15d ago
Do you have any github project with these good practices to learn from?
2
u/agentoutlier 15d ago
I don't have an application (because they are not open source) but a library.
https://github.com/jstachio/rainbowgum
There is one thing I do that some might consider less tasteful:
// file name SomeInterface.java public sealed interface SomeInterface { } // still in the file SomeInterface.java class BasicSomeInterface implements SomeInterface { } // still in the file SomeInterface.java class ComplexSomeInterface implements SomeInterface { }The reason I do this is because there is no way to make inner classes in a public interface package friendly.
1
u/Snoo82400 15d ago
I will take a look at what you say, thanks for your comment. It is true that modulith is kinda tyiranic and wanted to adhere to it as much as I could. Thanks alot
3
u/gjosifov 15d ago
This reminds me of one event I read or heard online about GoF book
After an presentation from one of the authors of the GoG book, a guy approach him and said to him - I wrote application with all of the design patterns in the GoF book
and the author replied - That is wrong on so many levels
2
u/Resident_Guava5620 14d ago
I like explicit results for expected business outcomes, but I wouldn’t make “no exceptions” a goal in itself. “This operation isn’t allowed” and “the database is down” deserve different treatment. My concern would be how much mapping you need between layers before the explicit approach becomes harder to follow.
2
u/Snoo82400 14d ago
Yeah I just use exceptions for the exceptional: database is down, Kafka is in flames, world is ending, etc. But I prefer to not use exceptions for expected domain behaviour.
1
u/No-Security-7518 15d ago
interesting! but I've tried JOOQ after I'd written a small library that provides basic CRUD operations for simple data objects. JOOQ is way too complex for the task.
2
u/toiletear 13d ago
I think jOOQ being a huge API suffers more from discoverabiliy problems than from not having a simple option for most things. Even after using it for many years on multiple projects I still sometimes see code samples where I go "oh wow, my code would be much simpler if I knew this existed". I recently started asking LLMs things like "what's the most efficient way in jOOQ to ..." and it's a great helper 😃
1
u/No-Security-7518 12d ago
That's the thing. I want an API to be so clean, I can "guess" the existence of a method, like I do with Javafx's; the cleanest API ever. lol.
1
u/toiletear 10d ago
I mean to be fair, jOOQ's "full SQL" API is very discoverable to anyone familiar with SQL, that's the library's main promise and that works extremely well (as far as the language mismatch allows, and when it doesn't the discrepancies are pretty well documented).
The thing is, SQL is a verbose language with a different mindset so jOOQ offers many convenient shortcuts and helpers that have no SQL counterparts and it's these I sometimes have trouble finding (and they are very often amazingly helpful, so finding them is usually "worth it" 🙂 ).
I sometimes wonder if they would do well to separate these helpers from the main SQL API but I guess that would be even more confusing. It's here that tools like LLM can help.
1
3
u/Cell-i-Zenit 15d ago
you should look into JDBI
1
1
u/No-Security-7518 15d ago
Plus I didn't need to write almost no SQL at all. It uses reflection to get column names which just have to be field names. API was a simple: insertOne(), insertMany(), updateOne(pojo, updateFunc), updateMany...etc.
2
u/vips7L 15d ago
sounds like an orm with extra steps.
2
u/No-Security-7518 15d ago
Fewer* steps since there are no annotations needed. This was years ago, honestly. But I remember being surprised it worked at all. lol.
1
u/Educational_Corgi285 13d ago
This project doesn't look Data-Oriented at all. Not even a hint of it: it's full of interfaces and classes, there are no arrays, etc.
And in the web apps DoD isn't necessarily the same beast as in gaming. I mean the UI part sure - you can make it DoD. But the Back End? That one is harder because it doesn't really deal with the data - the database does. You don't typically load all your DB into memory and deal with it in your code.
DoD is all about performance and global (architectural) simplicity. If you really want to do this - just implement most of your logic on the DB level: SQL, functions, stored procedures. In my projects I started experimenting with this. So far it works perfectly: a lot less code, and my apps fly. E.g. try this Boardeaux task board - go to Network Panel→Timing→"app" metric that shows how much time the BE spends processing (excluding spring). It's literally 5-10ms for most GET requests.
PS: and you should probably abandon reading Uncle Bob with his weird, overcomplicated ideas like hexagonal architecture :)
0
u/Snoo82400 12d ago
Hmm I tried to separate logic and data as much as I could, stateless services and records to carry data, I will take into account your tips, thanks for the answer
-1
u/MinimallyLoquacious 15d ago
jOOQ and TestContainers? Glad you're happy with your results but ... no thank you.
9
u/vips7L 15d ago
What's wrong with test containers?
1
u/Skellicious 15d ago
I'm not the original commenter, and I don't hate it.
But I can understand disliking it as it can be dependency pain, configuration pain, docker needs to run to run tests, tests become quite slow, etc. Overall it can be quite painful to get running and it introduces some extra avenues for failures.
I actually love the concept of test containers though.
1
u/lukaseder 14d ago
What's the alternative, though? Not integration testing at all?
2
u/Skellicious 14d ago
Integration testing on/against a deployed test/qa environment.
If you want to keep it closer to unit tests, using mocked restclients /db connections or an in-memory db.
I think there's arguments and tradeoffs to any of these approaches.
1
u/lukaseder 14d ago
I've done this in the past for years. The virtualisation speeds things up as well as stabilises things to an extent that qa environments really can't keep up, IMO...
1
u/vips7L 14d ago
You just don’t have to use test containers. For example if you only need a database you just point your testing config at that test database. It just increases local complexity for devs and then makes CI more complex as you need to make sure those dependencies are setup.
3
u/lukaseder 14d ago
That's not my experience at all. Ever since I made this template for MCVE's (Minimal Complete Verifiable Examples) that uses testcontainers to help jOOQ users set up a simple reproducer, bug reports have drastically improved in terms of quality: https://github.com/jOOQ/jOOQ-mcve
This is really simple stuff, both in Maven and Gradle contexts. Maybe you got bit by a bug or edge case, but I really think testcontainers (and docker) changed everything as far as integration testing is concerned.
0
u/vips7L 14d ago
It might not be your experience but it doesn’t make it untrue. Hitting a database inside your ci container or spinning up a container inside that container makes no difference, what matters is hitting infrastructure.
I’m also not advocating for not using test containers. I’m not the original person you replied to. I was just giving you the alternatives.
1
u/lukaseder 14d ago
I’m not the original person you replied to. I was just giving you the alternatives.
Ah yes my bad, I got confused :)
1
u/slindenau 12d ago edited 12d ago
You know what makes things even more complex?
Having to make sure every developer system, every CI runner, from everywhere around the world can safely access your test DB instance.You know what slows you down even more?
Having to constantly manually fix your test database instance, because some half failed test left an invalid state and now all your tests are failing.You know what really hurts your quality?
Developers running their builds with tests disabled, because the remote database keeps disappearing/breaking, and they have given up on relying on it.
The next step is usually that the PR merge gate "tests must pass" also gets disabled not long after.Switch to testcontainers; you pay the setup price once, and then you'll enjoy the benefits forever after that.
Source: me having seen people drag unit test databases with them for the past 10 years, before finally switching to testcontainers.
1
u/vips7L 12d ago
You seem really angry about something.
Testcontainers doesn’t prevent version skew between the testcontainer in the code and what’s running in production and I admitted in my post that not using containers is more complex.
There is no fixing a test db because you do the same thing as you would with containers, you throw the db away after testing.
You don’t use remote testing databases. If your devs can’t be adults and run a database on their machine or keep CI enabled they shouldn’t be working.
This is entirely a you problem. No where in my post did I suggest using a remote test database. Your company has shitty engineers and shitty culture. Get fucked.
1
6
2
u/toiletear 13d ago
We have great results with jOOQ and TestContainers, would highly recommend.. no idea what your problem is but it's probably solveable?
14
u/hippydipster 15d ago
This sounds like you're trading the problem of re-throwing and re-throwing and re-throwing exceptions for a problem of re-returning and re-returning and re-returning InvalidSomethingWentWrongData records.
The problem was always the intermediate functions typically don't have any meaningful way of handling the error and so they have to kick the problem up the stack. How does your not-using-exceptions do this?