Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Dependency Injection Without Frameworks

Posted on March 16, 2017 By Luis Fernandez

Dependency Injection Without Frameworks sounds like a dare, but it is really about writing code that reads like plain English and runs like a quiet engine.

Every week I see someone reach for a giant container just to pass a couple of collaborators into a constructor. I get it. Angular ships with an injector, Spring has lived in our heads for ages, and Node projects keep grabbing a container because it feels grown up. Still, most apps do not need a container. Most apps need a composition root and a few new operators. That is it. No magic strings, no global registries, no surprises in tests.

This is a simple pitch. Keep dependencies explicit, wire them in one place, and let the rest of your code be boring. Your future self will send you a thank you email from a caffeine filled morning when a bug report lands and the fix takes minutes. Today we have React and Vue on the front end, Node and Go and Java on the back, and cloud bits everywhere. The simpler the glue, the easier it is to move fast without breaking the story your code tells.

There is also the practical side. New teammates step into a codebase and ask where things come from. With a container the answer can be a long walk through config, decorators, and lifecycle rules. With plain code injection, the answer is right in the constructor or the function parameters. No container syntax to learn. No reflection tricks. Just code.

Let me show the shape of this with a few languages we keep using this month. Nothing fancy. No third party packages. Just a small pattern and a couple of files.

JavaScript and TypeScript

In Node land, modules are already a kind of injector. They return a value. They can accept collaborators. You can make a factory that builds a service with the exact things it needs. Here is a tiny example.

// emailService.js
module.exports = function makeEmailService(transport, logger) {
  return {
    sendWelcome: function(user) {
      logger.info('Sending welcome to ' + user.email);
      return transport.send({
        to: user.email,
        subject: 'Welcome',
        body: 'Hi ' + user.name
      });
    }
  };
};
// app.js
const smtp = require('./smtpTransport'); // your thin adapter
const logger = require('./logger');      // any logger
const makeEmailService = require('./emailService');

const emailService = makeEmailService(smtp, logger);

// route handler
function register(req, res) {
  // ... create user
  emailService.sendWelcome(user)
    .then(() => res.end('ok'))
    .catch(err => {
      logger.error(err);
      res.statusCode = 500; 
      res.end('fail');
    });
}

No container. No global singletons. The composition root is just the boot file. If you prefer TypeScript, add types and keep the shape the same.

// emailService.ts
export type Transport = { send: (msg: { to: string; subject: string; body: string }) => Promise<void> };
export type Logger = { info: (m: string) => void; error: (m: string) => void };

export function makeEmailService(transport: Transport, logger: Logger) {
  return {
    async sendWelcome(user: { name: string; email: string }) {
      logger.info(`Sending welcome to ${user.email}`);
      await transport.send({ to: user.email, subject: 'Welcome', body: `Hi ${user.name}` });
    }
  };
}

The key is to keep dependencies in parameters, not hidden behind a module level require. You can still share constants at the module level, just avoid pulling in a live service there. This keeps tests clean and keeps order of imports from changing behavior.

React and the front end

React already nudges you to pass things down. Props are injection. Context is a wider pipe but still explicit. If your app talks to a backend, pass a thin client down from the root. You can wrap it with hooks or with a tiny adapter but resist the global. This reads well, plays well with server side rendering, and makes test setup light.

// root.jsx
import { createRoot } from 'react-dom';
import { App } from './App';
import { makeApi } from './api';

const api = makeApi(fetch, window.localStorage);
createRoot(document.getElementById('root')).render(<App api={api} />);

// App.jsx
export function App({ api }) {
  return <Signup api={api} />;
}

// Signup.jsx
export function Signup({ api }) {
  const onSubmit = async data => {
    await api.signup(data);
  };
  return <form onSubmit={onSubmit}>...</form>;
}

You can store the client in context if prop drilling gets noisy, but still treat it as a value passed from the top. That keeps the story clear.

Java without a container

Spring is everywhere, and it gives you a lot. Still, you can do small services with plain Java and constructors. If you want that single place to wire things, create a Bootstrap class. Put all the new calls there. Pass interfaces into constructors. Done.

// EmailService.java
public class EmailService {
  private final Transport transport;
  private final Logger logger;

  public EmailService(Transport transport, Logger logger) {
    this.transport = transport;
    this.logger = logger;
  }

  public void sendWelcome(User user) {
    logger.info("Sending welcome to " + user.getEmail());
    transport.send(new Message(user.getEmail(), "Welcome", "Hi " + user.getName()));
  }
}
// Bootstrap.java
public class Bootstrap {
  public static App buildApp() {
    Transport transport = new SmtpTransport(...);
    Logger logger = new ConsoleLogger();
    EmailService emailService = new EmailService(transport, logger);
    return new App(emailService);
  }

  public static void main(String[] args) {
    App app = buildApp();
    app.run();
  }
}

No annotations needed. Your IDE can navigate all of it. Tests can pass a fake Transport or a fake Logger. The point is not to avoid Spring forever. The point is to reach for it only when it gives you something you truly want, not just to wire three classes.

A tiny composition root in Python

# app.py
class EmailService:
    def __init__(self, transport, logger):
        self.transport = transport
        self.logger = logger

    def send_welcome(self, user):
        self.logger.info('Sending welcome to ' + user['email'])
        self.transport.send(to=user['email'], subject='Welcome', body='Hi ' + user['name'])

def build_app():
    transport = SmtpTransport(...)
    logger = Logger()
    email_service = EmailService(transport, logger)
    return {'email_service': email_service}

That dictionary at the end might be your app context. You can pass bits of it to views or routes. Simple and clear.

Service locator is a trap

A service locator looks easy. You call get anywhere and hope the right thing appears. The cost shows up in tests and in surprise couplings. Hidden dependencies are the enemy of readability. With explicit injection the compiler or the linter will tell you when a piece is missing. With a locator you learn at runtime and often in a weird place.

Make the code read like a story

Give names to factories that sound like verbs. Keep constructors short. Prefer small interfaces over large grab bags. In JavaScript, return plain objects over deep classes unless you really need state and methods to travel together. In Java, prefer final fields and tiny types. In all languages, keep wiring at the top and keep logic in the leaves.

Tests get nicer with this setup. You pass a fake into the constructor or factory, then assert on calls or results. No container bootstrap in tests. No global reset in a before each. Just new and function calls. That speed shows up in your day. You run tests more often because they start fast and stay fast.

Why this matters this week

Angular is on a fast release train and people keep asking if they must take its injector outside of Angular apps. React keeps shipping small changes, and the current talk is about prop types and new warnings. Vue is winning fans with friendly docs. On the server side, Node keeps racing, Yarn is a daily tool, and Docker is the default on many laptops. With all this motion, a codebase that avoids framework lock in for wiring feels like a calm spot. You can upgrade libraries or even swap a stack without rewriting your object graph, because your graph is just code.

There is a time to bring a container. Huge apps with dynamic modules, plugins, conditional bindings that change per tenant, late binding for tools, these can justify it. Even then, keep a small composition root that shows how the whole thing fits. You can still use a container inside that function if you really need it. The top should remain readable.

If you try this, start by pulling your new calls into one file. Pass collaborators through parameters. Remove singletons one by one. Replace a hidden static with a value that moves through the call chain. With each step, your code gets easier to reason about. You will spot dead code and duplicated work. You will also see better names waiting to be written.

Rule of thumb: if you cannot explain where a dependency comes from in a sentence, wire it by hand first. If that still feels ugly after a few cycles, reach for a container with your eyes open.

Dependency Injection Without Frameworks is not a slogan. It is a reminder that you already have the tools. Functions. Constructors. Modules. Interfaces. A tiny bootstrap. With those you get clear flow, simple tests, and fewer surprises during upgrades. Keep the boring parts boring so you can spend your time on the parts that matter to users.

Keep it boring. Wire by hand.

Your code should read like a story.

Code Development Practices Software Engineering coding-practicesjavaspring

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes