by KronisLV | 2025-05-25 09:29:35+00:00
IoC is nice (or DI as a concept in particular), but DI frameworks/libraries sometimes are a mess.

I've had my fair share of Java and Spring Boot projects and it breaks in all sorts of stupid ways there, even things like the same exact code and runtime environment working in a container that's built locally, but not working when the "same" container is built on a CI server: https://blog.kronis.dev/blog/it-works-on-my-docker

Literally a case where Spring Boot DI just throws a hissy fit that you cannot easily track down, where I had to mess around with the @Lazy annotation (despite the configuration to permit that being explicitly turned on too) in over 100 places to resolve the issue, plus then when you try to inject a list of all classes that implement an interface with @Lazy it doesn't seem like their order is guaranteed either so your DefaultValidator needs to be tacked on to that list manually at the end.

Sorry about the Java/Spring rant.

It very much feels like the proper place for most DI is at compile time (like Dagger does for Java, seems closer to wire) not at runtime, or just keep IoC without a DI framework/library and having your code look a bit more like this:

@Override public void run(final BackendConfiguration configuration, final Environment environment) throws IOException, TimeoutException { // Initialize our data stores mariaDBManager = new MariaDBManager(configuration, environment); redisManager = new RedisManager(configuration); rabbitMQManager = new RabbitMQManager(configuration); // Initialize our generic services keyValueService = new KeyValueService(redisManager); sessionService = new SessionService(keyValueService, configuration); queueService = new QueueService(rabbitMQManager); // Initialize services needed by resources accountService = new AccountService(mariaDBManager); accountBalanceService = new AccountBalanceService(mariaDBManager); auctionService = new AuctionService(mariaDBManager); auctionLotService = new AuctionLotService(mariaDBManager); auctionLotBidService = new AuctionLotBidService(mariaDBManager); // Initialize background processes based on feature configuration if (configuration.getApplicationConfiguration().getFeaturesConfiguration().isProcessBids()) { bidListener = new BidListener(queueService, auctionLotBidService, auctionLotService, accountBalanceService); try { bidListener.start(); logger.info("BidListener started"); } catch (IOException e) { logger.error("Error starting BidListener: {}", e.getMessage(), e); } } // Register resources based on feature configuration if (configuration.getApplicationConfiguration().getFeaturesConfiguration().isAccounts()) { environment.jersey().register(new AccountResource(accountService, accountBalanceService, sessionService, configuration)); } if (configuration.getApplicationConfiguration().getFeaturesConfiguration().isBids()) { environment.jersey().register(new AuctionResource( auctionService, auctionLotService, auctionLotBidService, sessionService, queueService)); } ... }

Just a snippet of code from a Java Dropwizard example project, not all of its contents either, but should show that it's nothing impossibly difficult. Same principles apply to other languages and tech stacks, plus the above is unequivocally easier to put a breakpoint in and debug, vs some dynamic annotation or convention based mess.

Overall, I agree with the article, even across multiple languages.