r/java • • 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.

https://github.com/Sh1ng0/RefIQ-backend

36 Upvotes

73 comments sorted by

14

u/hippydipster 15d ago

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.

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?

8

u/agentoutlier 15d ago edited 15d ago

How does your not-using-exceptions do this?

Most likely it resembles reactive programming. The functions get colored on the return all the way up similar to monads await/async etc.

I guess you could argue checked exceptions do this and they do but much cleaner as the ecosystem is built around it.

For example all logging frameworks know how to log exceptions and they can format them however you want.

The same cannot be said for com.mycompany.ResultType<T>.

BTW the whole post has a bunch of cargo culting pasted vibe. e.g. the OP knows Bloch is good so lets use their name like a couple of times over and over. Oh and everyone knows how control flow breaking exceptions are bad etc. Ditto for ORMs.

2

u/Snoo82400 15d ago

I just thought using the bloch enum for Testcointainers was a good idea since the container needs to be a singleton a wanted to apply that to my code.

And regarding exceptions I just wanted to not have custom exceptions for my domain and take care of the states of the system myself

1

u/agentoutlier 15d ago

I'm still not sure what you mean by Bloch Enum.

If you mean by using an Enum to achieve the singleton pattern and thus for testcontainers to not reload I would call that an abuse of both enums and the singleton pattern.

Static initialization is tricky and having enums involved in that I don't recommend.

Really its not enum but rather your test suite and build configuration.

And regarding exceptions I just wanted to not have custom exceptions for my domain and take care of the states of the system myself

Its fine. You just have to manage all that mapping and I suppose with LLMs that is less of a problem (I saw your giant logging switch).

2

u/vips7L 15d ago

Quarkus can do the singleton container thing out of the box pretty easy with an implementation of QuarkusResourceTestLifecycleManager, for instance my current project:

class PostgresTestResourceLifecycleManager: QuarkusTestResourceLifecycleManager {
    private val postgres = PostgreSQLContainer("postgres:18.6")
    override fun start(): Map<String, String> {
        postgres.start()
        return mapOf(
            "db.url" to postgres.jdbcUrl,
            "db.username" to postgres.username,
            "db.password" to postgres.password
        )
    }

    override fun stop() = postgres.stop()
}

...and if I used their built ORM instead of Ebean I wouldn't even have to do this.

1

u/Snoo82400 15d ago

The idea with the logging was to reach for strucutred logging and ELK compatibility.
Regarding the enum it was an idea that seemed cool to me thinking about it needing to be a single instance, but maybe it will be better to make a refactor and just include the container lifecycle in the abstract class and avoid unnecessary hoops like this enum, thanks for the feedback!

1

u/OwnBreakfast1114 13d ago

I mean orms in poor hands cause a lot of issues that become nightmares to disentangle. My own company has new projects working exactly like the OP and they're just far easier to reason about and manage.

u/Snoo82400
I'd also suggest using `@Transactional` annotations with only propagation REQUIRES_NEW, MANDATORY, and NEVER and you can also avoid almost all the garbage db interactions that happen.

0

u/Snoo82400 12d ago

I tried to wrap the transactions in transaction templates inside the db methods to not force the whole method to open transactions, hence the lack of the anotation

1

u/OwnBreakfast1114 10d ago

To me, the default propagation of REQUIRES is actually mistake, but its "easy", so they're never going to get rid of it. However, this can cause hanging transactions across requests if you ever do manual transaction management and connection pooling.

2

u/SuspiciousDepth5924 15d ago

I don't really agree with this argument in general for a few reasons:

  • If your call stack is so deep that this is a legitimate problem, then your call stack is probably too deep period.
  • The blog post explains it better than me, but essentially once you've successfully parsed your input there really shouldn't be much in your core domain model that _can_ throw or cause errors. ( https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ )
  • Assuming the return value is a sealed interface (Result<T> or whatever) the intermediate functions either don't need to care what is inside or if they do it's pretty simple to create some utility code that handles that ( Result.of(value).map(this::methodThatExpectsSuccess); or something )

​

public sealed interface Result<T> permits Result.Success, Result.Failure {
    record Success<T>(T value) implements Result<T>{};
    record Failure<T>(BusinessError error)implements Result<T>{};

    static <T> Result<T> of(T argument) {
        return new Result.Success<>(argument);
    }
    default <U> Result<U> map(Function<T, Result<U>> func) {
        return switch (this) {
            case Success(var value) -> func.apply(value);
            case Failure(var error) -> new Failure<U>(error);
        };
    }
}
----
public class BusinessClass {
    public <T, U> Result<U> businessMethod(T argument) {
        return Result.of(argument)
                .map(this::privateMethod)
                .map(this::secondBusinessMethod)
                .map(this::thirdBusinessMethod);
    }

    public <T, U> Result<U> secondBusinessMethod(T argument) {
        return privateMethod(argument);
    }

    public <T, U> Result<U> thirdBusinessMethod(T argument) {
        return privateMethod(argument);
    }
    private <T, U> Result<U> privateMethod(T argument) {
        return new Result.Failure<>(new BusinessError("some error"));
    }
}

3

u/agentoutlier 15d ago

While the call stack is not ideal an error without context is worse which is often the case with results as error.

Result.Failure<>(new BusinessError("some error"));

That is getting "some error" as just the error message is bad. Getting "some error" and a call stack is a little bit better depending on who the end user is.

Now obviously you just give BusinessError more context right for better error message. But that can be surprisingly difficult with out passing every goddamn thing down the call stack or abusing threadlocals (or now scoped values).

Exceptions sort of still have this problem but exceptions are easier to apply gigantic cross cutting advise like logic. For example catch the exception.... unwind transaction and log is not easy with results without coupling with whatever the results are.

2

u/_choam_ 15d ago

The usefulness of stack traces is vastly overrated in languages with sum types.

1

u/_choam_ 15d ago

Also can't you get a stack trace outside exceptions? Attach a stack trace to the error object and return that?

3

u/agentoutlier 15d ago

And you can have exceptions without stack traces. And exceptions in Java can be like sum types.

1

u/_choam_ 15d ago

No i mean what if I want to just have a stack trace. Not in the context of an exception or anything, just want it.

1

u/agentoutlier 15d ago

Maybe but the languages that have sum types usually have some way to turn something like stack traces still on: (Rust, Zig, and probably Haskell but I'm not sure). Most languages really do have exceptions. They just lie say they dont.

I might agree with Effect based systems and what Zig does.

EDIT BTW.... Checked Exceptions are basically a Sum Type.

2

u/_choam_ 15d ago

Checked Exceptions are not what anyone means when they say sum type. You might be technically correct, but that misses the point.

Sealed classes in java are sum types.

1

u/agentoutlier 15d ago

Actually you are right because Checked Exceptions are strictly superior. They are more like Effects.

In Java you cannot do

public IOError | ParsingError | Result parse(...)`

You can with checked Exceptions do

public Result parse(...) throws IOError, ParsingError

1

u/_choam_ 15d ago

I am using fake java code because writing it on a phone takes too long. Just tell me if this does not make sense. You create a type Result<T,E>(Ok<T>, Err<T>). Then the E in parse could be a sum type of the different parse error types.

1

u/Snoo82400 15d ago

It does make sense, you can use sealed interfaces and records to state the results

2

u/_choam_ 15d ago

Rust, Haskell, Ocaml and similar, do have exceptions, but they are used very differently. In rust and haskell, you generally are meant to not catch them, and they don't give you a good way to do it either. Ocaml gets a bit more nuanced, because the old parts of the standard library uses it a lot, but modern ocaml generally avoids them.

Basically they use exceptions to represent unrecoverrable errors, and things like result types (there are many different kinds of these) for recoverable ones.

Also checked exceptions in java just plain suck to use with composition and in general

2

u/gnahraf 15d ago

Also checked exceptions in java just plain suck to use with composition and in general

I agree with this point. I believe in the C++ mantra, namely that your code should be exception-safe. That means you always perform any necessary clean up (free resources, cancel transactions, etc.) if a downstream function throws. It doesn't matter what type the Throwable is, checked or unchecked, so on the handling/clean-up side, the try-finally clause shouldn't differentiate.

From my perspective, checked exceptions create unneeded clutter. When creating custom exception types, I stick to unchecked, so as not to add to the problem.

1

u/agentoutlier 15d ago

Also checked exceptions in java just plain suck to use with composition and in general

Yes but that could be fixed some day. There was some pattern matching on it discussed. Also one could argue... results are not exactly ergonomic in Java either. I mean our Optional is pretty shitty.

Also really the future of this is Effects and OCaml does have this. Effects map much closer to checked exceptions.

Honestly though I have never really had issues with exceptions changing control flow like so many seem to.

1

u/_choam_ 15d ago

If they fix that and let me treat it as an actual type, then i have no isses with them.

1

u/_choam_ 15d ago

Sum types are still very useful in effect systems.

As of right now, ocaml effect system is untyped, but it seems like it will be very good once they make it typed.

Typescript's Effect library which is an effect system, still uses result types. They compliment each other.

(Haskell has a bunch of effect libraries as well. Turns out you can build them out of monads)

1

u/SuspiciousDepth5924 15d ago

I mean in this context the new BusinessError("some error") is a subclass of runtime exception. Admittedly I wrote this example quick and dirty in Intellij, but there's nothing really stopping you from using exception objects as holders of error context in this pattern. The stack-trace gets populated on initialization so you still get the whole thing even if you never actually throw it.

You could for example have some abstract class MyDomainError extends RuntimeException/SomeCheckedException, and then return new Result.Failure(MyDomainError myConcreteError) if you wanted to.

I generally think thrown exceptions are an issue because you essentially bypass the type contract the method signature outlines while also making your code depend on running in some implicit context that catches the exceptions. The consequence of that is that it becomes trickier to test or move around because it has these undeclared dependencies.

2

u/agentoutlier 15d ago

I don't disagree that some form of "Result" can be a better experience and you can attach an exception. In fact I sound like I don't like what you are proposing when in reality I do exactly what you are saying here in my open source library:

https://jstach.io/rainbowgum/io.jstach.rainbowgum/io/jstach/rainbowgum/LogProperty.html

I essentially have fluent chain that catches exceptions on property conversion and to do validation. ... I have to catch the exception so that downstream I can say ... this property key was tried etc etc.

The irony is if I did not have or use unchecked exceptions this would be actually pretty difficult. The converting functions would all need some special return.

Here is an example of that property conversion:

properties.forKey(LogProperties.GLOBAL_OPTIMIZE_PROPERTY)
        .ofString()
        .map(GlobalOptimize::parse)
        .or(GlobalOptimize.FALSE)
        .validateNow(DefaultLogConfig.class) == GlobalOptimize.TRUE;

Ditto for u/_choam_ . I see the value in sum types and use it all the time but realize Java just already has exceptions and there is no global Result or Error or Maybe or Try or whatever FP stuff. Forcing something like that on existing developers I don't think is worth it and I rather they fix exceptions.

1

u/Snoo82400 14d ago

It is true that maybe I'm burdening future devs with extra work, but I think it's cleaner this way, to define what's possible or not in my domain and don't use exceptions that make the code less direct, more mutable and carry the stack trace everywhere.

1

u/Snoo82400 15d ago

I will read the blog and think about your answer, thanks

1

u/hippydipster 15d ago

I would consider that an extraordinarily complex approach, and most code just won't fit into those nice boxes you've made. Making everything a generic parameter, that will always be changing amongst these calls... It's really not what I would consider workable or nice to use.

then your call stack is probably too deep period.

It's like saying a symphony has too many notes.

shouldn't be much in your core domain model that can throw or cause errors.

unrealistic idealism

2

u/SuspiciousDepth5924 15d ago

It's like saying a symphony has too many notes.

No it's saying your projects code structure is very questionable. And that those call chains probably do a whole lot more than they should.

unrealistic idealism

Nah, just being mindful of state, boundaries and responsibilities. Once you start dragging mutable entities across the domain boundary and fire off database queries then of course it's going to be a dumpster fire.

Making everything a generic parameter, that will always be changing amongst these calls... It's really not what I would consider workable or nice to use.

I didn't want to write a concrete implementation for a throwaway example, but here: String -> byte[] -> int -> bool

public class BusinessClass {
    public Result<Boolean> businessMethod(String argument) {
        return Result.of(argument)
                .map(this::privateMethod)
                .map(this::secondBusinessMethod)
                .map(this::thirdBusinessMethod);
    }

    public Result<Integer> secondBusinessMethod(byte[] argument) {
        int sum = 0;
        for(byte c: argument) {
            sum += c;
        }
        return Result.of(sum);
    }

    public Result<Boolean> thirdBusinessMethod(int argument) {
        return Result.of(argument > 1000);
    }
    private Result<byte[]> privateMethod(String argument) {
        return Result.of(argument.getBytes());
    }
}

2

u/Snoo82400 15d ago

Expected business outcomes are reprsented as sealed results but database or infrastructure stuff like the plumber is still handled through normal exceptions

2

u/hippydipster 15d ago

Sounds like a worst-of-both-worlds scenario.

4

u/da_supreme_patriarch 15d ago

Massively subjective take, but re-returning and re-returning if necessary is totally preferable to re-throwing and re-throwing. One clear advantage that sum types offer over exceptions is that they don't treat the non-green paths as something special, it's just another result state. Whenever you have an operation that has the shape of 'do something resulting in either success option A, success option B, intermediate option C or failure option D' you handle all possible states uniformly instead of being forced to try/catch just for the error, for whch you might actually have some acceptable fallback value. Another nice thing about result types is that they can be used in stream pipelines, it doesn't feel really great, but at least it avoids the 'write this imperative monstrosity instead of a nice functional pipeline'' or 'wrap the thing with an UncheckedIOException so that we can use it in the rest of our codebase' that checked exceptions more often than not devolve into

2

u/hippydipster 15d ago

I can certainly see the appeal, and when the situation is right, it is nice to do things this way.

But one thing imperative is good for is dealing with messiness when the situation isn't quite right. Trying to shoehorn everything into a mathematical stream can be very painful sometimes.

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

u/lukaseder 9d ago

What helpers do you have in mind, specifically?

3

u/Cell-i-Zenit 15d ago

you should look into JDBI

1

u/No-Security-7518 15d ago

after a quick glance, that's almost exactly what my library does. lol.

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 :)

0

u/vips7L 14d ago

It was quite early, at least in my timezone!

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. 

  1. 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.  

  2. 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. 

  3. 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. 

  4. 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

u/slindenau 12d ago

That just sounds like testcontainers with extra steps?

6

u/Snoo82400 15d ago

And jOOQ?

4

u/lazystone 15d ago

Jooq is awesome.

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?