🍦 Vanilla DI

The world's most advanced zero-dependency Java Dependency Injection framework

The numbers don’t lie!

Vanilla DI is already used by more Java applications than Spring, Spring Boot, Guice, Dagger, CDI, and PicoContainer - combined.

With 100% market penetration across all Java environments and flawless backward compatibility to Java 1.0, Vanilla DI represents the pinnacle of dependency injection evolution.

Ready to make the switch?

Stop spending weeks mastering arcane framework configuration. Join the millions of developers who’ve discovered the revolutionary simplicity of Vanilla DI!

  • Final JAR size: 0 bytes uncompressed, 0 bytes compressed
  • Memory footprint: Negative bytes (actually reduces your application size)
  • Learning curve: Approximately 3 minutes (including coffee break)

Explore Vanilla Libraries

Ready for more vanilla goodness? Check out our Vanilla Libraries - copy/paste ready Java code snippets that replace entire frameworks:

  • Vanilla Result - Treat errors as values (157 lines vs 50,000 lines of Vavr)
  • More coming soon…

Getting Started

Add Vanilla DI to your project in seconds:

Maven

<dependency>
  <groupId>org.acme</groupId>
  <artifactId>vanilla-di</artifactId>
  <version>1.0.0</version>
</dependency>

Gradle

implementation 'org.acme:vanilla-di:1.0.0'

Going to Production?

When you’re ready to deploy to production, simply remove the dependency from your pom.xml or build.gradle.

That’s it!

Vanilla DI is so advanced that it actually works better without being installed. The framework achieves peak performance through quantum dependency injection - dependencies exist in a superposition of injected and not-injected until observed by your constructor.

Advanced Configuration

For enterprise deployments requiring maximum sophistication:

// Behold: the complete Vanilla DI configuration
class MyApplication {
  void main() {
    // Configuration complete
    // No XML, no YAML, no annotations, no tears
  }
}

Pro tip: Each framework you don’t add increases Vanilla DI performance exponentially!

Why choose Vanilla DI?

Quantum Performance

Achieves impossible speeds by not doing anything. Zero reflection, zero proxies, zero framework overhead. Benchmarks show startup times so fast they complete before you run them.

Negative Dependencies

Not only zero dependencies - Vanilla DI actually *removes* other frameworks from your classpath through advanced bytecode subtraction algorithms.

Radical Transparency

What you see is literally what you get. No annotations, no magic, no surprises, no framework documentation to read. Just constructors doing what constructors do.

Anti-Configuration

Vanilla DI pioneered the revolutionary "No Configuration" paradigm. No XML, no YAML, no annotations, no confusion. Just Java that any developer can understand.

Performance Comparison

Don’t just take our word for it! Here’s how Vanilla DI completely dominates other so-called “frameworks” in scientifically rigorous benchmarks:

Framework Startup Time* Memory Usage** JAR Size*** Reflection Calls Magic Level
Vanilla DI 3ms ~22MB base 0 bytes 0 None
Dagger 2 46ms ~25MB + app ~1-3MB Zero**** Low 🃏
Google Guice 458ms ~30-40MB + app ~2-5MB Hundreds Medium 🎩
Quarkus 0.5-1.5 seconds ~180MB heap 15-25MB JAR Minimal***** Medium-Low 🎭
Micronaut 1-2 seconds 254MB heap 12MB JAR Minimal Medium 🎪
Spring Framework 7-13 seconds 305MB heap 24MB+ JAR Thousands High 🪄
Spring Boot 3-7 seconds 305MB+ heap 24-50MB JAR Thousands Very High 🔮
CDI/Jakarta EE 10-20 seconds 400-600MB+ 50-200MB+ Thousands Extreme 🧙‍♂️

Data Sources & Disclaimers

*Startup times from DI Framework Benchmarks and various 2024 performance studies

**Memory usage includes heap allocation after startup for simple applications (Spring Boot data from official benchmarks)

***JAR sizes for minimal applications with basic DI functionality

****Dagger 2 uses compile-time generation, so runtime reflection is zero, but requires annotation processing

*****Quarkus performs CDI processing at build time, significantly reducing runtime reflection compared to traditional CDI

Important Notes:

  • Numbers vary significantly based on application complexity, dependencies, and JVM settings
  • Vanilla DI baseline includes minimal JVM overhead for object creation
  • Framework memory includes both heap and non-heap usage
  • Performance can be optimized through configuration and architectural choices
  • Real-world applications may see different results based on usage patterns

What these numbers really mean:

  • Startup Time: How long you wait before your application actually starts doing work
  • Memory Overhead: RAM consumed by the framework itself (not your business logic)
  • JAR Size: Additional bloat in your deployment artifacts
  • Reflection Calls: Runtime introspection that slows down your code
  • Magic Level: How much invisible behavior happens behind your back

Real-world Impact:

// Vanilla DI application startup (benchmarked at ~3ms for DI container creation)
class Application {
  void main() {
  // ~3ms: Create your objects (actual measured time)
  var config = new DatabaseConfig("jdbc:postgresql://localhost/mydb");
  var repository = new UserRepository(config);
  var service = new UserService(repository);

  // Additional time for server startup (not DI-related)
  startServer(service);
  IO.println("Application ready!"); // <- DI part is nearly instant
}
// Spring Boot application startup (benchmarked at 3-7 seconds typical)
@SpringBootApplication
class Application {
  void main() {
    // Based on actual Spring Boot benchmark measurements:
    // 0-1000ms: JVM startup and class loading
    // 1000-3000ms: Scanning classpath for components
    // 3000-4000ms: Creating bean definitions and resolving dependencies
    // 4000-5000ms: Initializing application context and proxies
    // 5000-7000ms: Post-processors and auto-configuration
    SpringApplication.run(Application.class, args);
    // 7000ms+: Application ready (measured on simple apps)
  }
}

Benchmark Context: These measurements were obtained through rigorous scientific methodology involving stopwatches, prayer, and occasionally asking the JVM nicely. Results independently verified by the International Bureau of Framework Benchmarking.

See the stunning simplicity!

Witness the dramatic difference between framework complexity and Vanilla DI elegance. Prepare to be amazed by the power of doing less:

Basic Service Injection

Spring Framework

Complexity: 8/10
@Service
class UserService {
  @Autowired
  UserRepository userRepository;

  User findUser(Long id) {
    return userRepository.findById(id);
  }
}

@Repository
class UserRepository {
  @Autowired
  EntityManager entityManager;

  User findById(Long id) {
    return entityManager.find(User.class, id);
  }
}

@Configuration
@ComponentScan
@EnableJpaRepositories
class AppConfig {
  // Complex configuration...
}

Vanilla DI

Complexity: 0/10
class UserService {
  UserRepository userRepository;

  UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  User findUser(Long id) {
    return userRepository.findById(id);
  }
}

class UserRepository {
  EntityManager entityManager;

  UserRepository(EntityManager entityManager) {
    this.entityManager = entityManager;
  }

  User findById(Long id) {
    return entityManager.find(User.class, id);
  }
}

// In your main method or factory:
var entityManager = createEntityManager();
var repository = new UserRepository(entityManager);
var service = new UserService(repository);

Marvel at this breakthrough in software engineering

Complex Dependency Graph

Google Guice

Complexity: 6/10
class DatabaseModule extends AbstractModule {
  @Override
  void configure() {
    bind(UserRepository.class).to(JpaUserRepository.class);
    bind(OrderRepository.class).to(JpaOrderRepository.class);
    bind(EmailService.class).to(SmtpEmailService.class);
  }

  @Provides
  @Singleton
  DataSource provideDataSource() {
    // Complex datasource configuration
  }
}

class OrderService {
  @Inject
  UserRepository userRepository;

  @Inject
  OrderRepository orderRepository;

  @Inject
  EmailService emailService;

  void processOrder(Order order) {
    // Business logic
  }
}

// Bootstrap
Injector injector = Guice.createInjector(new DatabaseModule());
OrderService orderService = injector.getInstance(OrderService.class);

Vanilla DI

Complexity: 0/10
class OrderService {
  UserRepository userRepository;
  OrderRepository orderRepository;
  EmailService emailService;

  OrderService(UserRepository userRepository,
               OrderRepository orderRepository,
               EmailService emailService) {
    this.userRepository = userRepository;
    this.orderRepository = orderRepository;
    this.emailService = emailService;
  }

  void processOrder(Order order) {
    // Business logic
  }
}

// In your application factory:
var dataSource = createDataSource();
var userRepository = new JpaUserRepository(dataSource);
var orderRepository = new JpaOrderRepository(dataSource);
var emailService = new SmtpEmailService();

var orderService = new OrderService(
  userRepository,
  orderRepository,
  emailService
);

Behold the revolutionary concept of 'new' - a cutting-edge Java keyword that creates objects! No PhD in Guice binding semantics required.

Conditional Dependencies

Spring Boot

Complexity: 9/10
@Service
@Profile("development")
class MockEmailService implements EmailService {
  void sendEmail(String to, String message) {
    System.out.println("Mock email: " + message);
  }
}

@Service
@Profile("production")
class SmtpEmailService implements EmailService {
  @Value("${smtp.host}")
  String smtpHost;

  @Value("${smtp.port}")
  int smtpPort;

  void sendEmail(String to, String message) {
    // Real SMTP implementation
  }
}

@RestController
class NotificationController {
  @Autowired
  EmailService emailService; // Magic injection

  @PostMapping("/notify")
  void notify(@RequestBody NotificationRequest request) {
    emailService.sendEmail(request.getEmail(), request.getMessage());
  }
}

# Plus separate property files for each environment

Vanilla DI

Complexity: 0/10
interface EmailService {
  void sendEmail(String to, String message);
}

class MockEmailService implements EmailService {
  void sendEmail(String to, String message) {
    System.out.println("Mock email: " + message);
  }
}

class SmtpEmailService implements EmailService {
  String smtpHost;
  int smtpPort;

  SmtpEmailService(String smtpHost, int smtpPort) {
    this.smtpHost = smtpHost;
    this.smtpPort = smtpPort;
  }

  void sendEmail(String to, String message) {
    // Real SMTP implementation
  }
}

class NotificationController {
  EmailService emailService;

  NotificationController(EmailService emailService) {
    this.emailService = emailService;
  }

  void notify(NotificationRequest request) {
    emailService.sendEmail(request.getEmail(), request.getMessage());
  }
}

// In your application factory:
class ApplicationFactory {
  static NotificationController createController(boolean isDevelopment) {
    var emailService = isDevelopment ?
      new MockEmailService() :
      new SmtpEmailService("smtp.company.com", 587);

    return new NotificationController(emailService);
  }
}

Witness the ancient art of 'if statements' - a mystical Java technique that eliminates the need for @Profile annotations and magical property injection!

Testing with Mocks

Dagger 2

Complexity: 4/10
@Component(modules = {DatabaseModule.class})
@Singleton
interface ApplicationComponent {
  UserService userService();
}

@Module
class TestDatabaseModule {
  @Provides
  @Singleton
  UserRepository provideUserRepository() {
    return Mockito.mock(UserRepository.class);
  }
}

class UserServiceTest {
  @Test
  void testFindUser() {
    // Complex test component setup
    ApplicationComponent component = DaggerApplicationComponent.builder()
      .databaseModule(new TestDatabaseModule())
      .build();

    UserService service = component.userService();
    // Test logic...
  }
}

Vanilla DI

Complexity: 0/10
class UserServiceTest {
  @Test
  void testFindUser() {
    // Create test dependencies
    var mockRepository = Mockito.mock(UserRepository.class);
    var expectedUser = new User(1L, "John");

    when(mockRepository.findById(1L)).thenReturn(expectedUser);

    // Create service with test dependencies
    var service = new UserService(mockRepository);

    // Test
    var result = service.findUser(1L);

    // Verify
    assertEquals(expectedUser, result);
    verify(mockRepository).findById(1L);
  }
}

Shocking revelation

Cloud-Native Framework Comparison

Micronaut

Complexity: 5/10
@Singleton
class UserService {
  UserRepository userRepository;
  NotificationService notificationService;

  UserService(UserRepository userRepository,
              NotificationService notificationService) {
    this.userRepository = userRepository;
    this.notificationService = notificationService;
  }

  @Executable
  CompletableFuture<User> createUserAsync(CreateUserRequest request) {
    return CompletableFuture.supplyAsync(() -> {
      User user = new User(request.getName(), request.getEmail());
      User savedUser = userRepository.save(user);
      notificationService.sendWelcomeEmail(savedUser);
      return savedUser;
    });
  }
}

@Singleton
@Requires(property = "app.notifications.enabled", value = "true")
class EmailNotificationService implements NotificationService {
  @Value("${smtp.host}")
  String smtpHost;

  @PostConstruct
  void init() {
    // Initialize SMTP connection
  }
}

// Application startup
@Application
class MicronautApp {
  void main() {
    // Framework handles dependency injection
    Micronaut.run(MicronautApp.class, args);
  }
}

Vanilla DI

Complexity: 0/10
class UserService {
  UserRepository userRepository;
  NotificationService notificationService;
  ExecutorService executorService;

  UserService(UserRepository userRepository,
              NotificationService notificationService,
              ExecutorService executorService) {
    this.userRepository = userRepository;
    this.notificationService = notificationService;
    this.executorService = executorService;
  }

  CompletableFuture<User> createUserAsync(CreateUserRequest request) {
    return CompletableFuture.supplyAsync(() -> {
      User user = new User(request.getName(), request.getEmail());
      User savedUser = userRepository.save(user);
      notificationService.sendWelcomeEmail(savedUser);
      return savedUser;
    }, executorService);
  }
}

class EmailNotificationService implements NotificationService {
  String smtpHost;

  EmailNotificationService(String smtpHost) {
    this.smtpHost = smtpHost;
    // Initialize SMTP connection in constructor
  }
}

// Application startup
class Application {
  void main() {
    // Explicit dependency creation and injection
    var smtpHost = System.getProperty("smtp.host", "localhost");
    var notificationsEnabled = Boolean.parseBoolean(
      System.getProperty("app.notifications.enabled", "true"));

    var notificationService = notificationsEnabled ?
      new EmailNotificationService(smtpHost) :
      new NoOpNotificationService();

    var executorService = Executors.newFixedThreadPool(10);
    var userRepository = new JpaUserRepository();

    var userService = new UserService(
      userRepository,
      notificationService,
      executorService);

    startServer(userService);
  }
}

Discover the radical concept of reading configuration values and passing them to constructors - a technique so simple it predates cloud computing!

Enterprise Java Stack

Jakarta EE + CDI

Complexity: 10/10
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
class OrderProcessingService {
  @EJB
  InventoryService inventoryService;

  @EJB
  PaymentService paymentService;

  @Inject
  AuditLogger auditLogger;

  @Resource
  UserTransaction userTransaction;

  @PersistenceContext(unitName = "ordersPU")
  EntityManager entityManager;

  @Resource(lookup = "jms/OrderQueue")
  Queue orderQueue;

  @Resource(lookup = "jms/QueueConnectionFactory")
  QueueConnectionFactory queueConnectionFactory;

  @Asynchronous
  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  Future<OrderResult> processOrderAsync(
          @Valid OrderRequest request,
          @NotNull SecurityContext securityContext) {

    try {
      // Framework manages transactions, security, validation
      Order order = new Order(request);
      entityManager.persist(order);

      inventoryService.reserveItems(order.getItems());
      PaymentResult payment = paymentService.processPayment(order);

      sendOrderNotification(order);
      auditLogger.logOrderProcessed(order, securityContext.getUserPrincipal());

      return new AsyncResult<>(new OrderResult(order, payment));
    } catch (Exception e) {
      // Container handles rollback
      throw new EJBException("Order processing failed", e);
    }
  }

  void sendOrderNotification(Order order) throws JMSException {
    try (QueueConnection connection = queueConnectionFactory.createQueueConnection()) {
      QueueSession session = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
      QueueSender sender = session.createSender(orderQueue);

      ObjectMessage message = session.createObjectMessage(new OrderNotification(order));
      sender.send(message);
    }
  }
}

// Plus: persistence.xml, ejb-jar.xml, web.xml configuration
// Plus: Application server deployment descriptors
// Plus: JNDI resource configuration

Vanilla DI

Complexity: 0/10
class OrderProcessingService {
  InventoryService inventoryService;
  PaymentService paymentService;
  AuditLogger auditLogger;
  TransactionManager transactionManager;
  EntityManager entityManager;
  MessageProducer messageProducer;
  ExecutorService executorService;

  OrderProcessingService(
          InventoryService inventoryService,
          PaymentService paymentService,
          AuditLogger auditLogger,
          TransactionManager transactionManager,
          EntityManager entityManager,
          MessageProducer messageProducer,
          ExecutorService executorService) {
    this.inventoryService = inventoryService;
    this.paymentService = paymentService;
    this.auditLogger = auditLogger;
    this.transactionManager = transactionManager;
    this.entityManager = entityManager;
    this.messageProducer = messageProducer;
    this.executorService = executorService;
  }

  CompletableFuture<OrderResult> processOrderAsync(
          OrderRequest request,
          User currentUser) {

    return CompletableFuture.supplyAsync(() -> {
      return transactionManager.executeInTransaction(() -> {
        // Validate request manually (or use bean validation)
        validateOrderRequest(request);

        var order = new Order(request);
        entityManager.persist(order);

        inventoryService.reserveItems(order.getItems());
        var payment = paymentService.processPayment(order);

        messageProducer.sendOrderNotification(new OrderNotification(order));
        auditLogger.logOrderProcessed(order, currentUser);

        return new OrderResult(order, payment);
      });
    }, executorService);
  }

  void validateOrderRequest(OrderRequest request) {
    if (request == null || request.getItems().isEmpty()) {
      throw new IllegalArgumentException("Invalid order request");
    }
  }
}

// In your application factory:
var orderService = new OrderProcessingService(
  new DatabaseInventoryService(dataSource),
  new StripePaymentService(stripeApiKey),
  new DatabaseAuditLogger(dataSource),
  new JpaTransactionManager(entityManager),
  entityManager,
  new JmsMessageProducer(connectionFactory, orderQueue),
  Executors.newFixedThreadPool(20)
);

Experience the jaw-dropping simplicity of creating objects with 'new' and passing them around. No application servers, no JNDI wizardry, no enterprise consultants required!


Support Vanilla DI Development

Maintaining the world's most advanced zero-dependency framework is surprisingly challenging! Help us sustain our revolutionary 0-byte codebase and continue not implementing features.

Buy Me A Coffee

Your contribution helps us:

  • Maintain our cutting-edge negative dependency count
  • Fund our research into quantum bytecode subtraction
  • Support our elite team of 0 full-time framework architects
  • Continue our groundbreaking "anti-documentation" initiative
  • Achieve our goal of sub-zero startup times

Fun fact: 100% of donations are immediately invested in not implementing features!

The Vanilla DI team meticulously maintains all zero lines of code in the framework and works tirelessly each day to ensure nothing is added, changed, or improved.

Made with ❤️ for Java developers who’ve grown tired of framework complexity and yearn for the days when new was enough.

Inspired by the excellent Vanilla JS and powered by constructor injection since 1995