Post

Singleton, Scoped, Transient (DI Lifetimes)

Singleton, Scoped, Transient (DI Lifetimes)

Introduction

Dependency Injection (DI) lifetimes answer one practical question: when should a dependency be created, and who should share it? This post gives an intuitive mental model, real examples, and a quick way to choose between singleton, scoped, and transient.

Quick Code Examples (Java)

These are intentionally minimal, to show the idea, not a full framework setup.

Singleton (one instance for the whole app):

1
2
3
4
class AppContainer {
  private final Logger logger = new Logger(); // created once
  Logger logger() { return logger; }
}

Scoped (one instance per request/operation):

1
2
3
4
class RequestScope {
  private final DbContext db = new DbContext(); // one per request
  DbContext db() { return db; }
}

Transient (new instance every time):

1
2
3
class AppContainer {
  Validator validator() { return new Validator(); } // new each call
}

The Short Version

Singleton, scoped, and transient are not “best practices” so much as tradeoffs about how long an instance should live and who should share it. The right choice depends on whether the instance holds shared state, must be thread-safe, or must be isolated to a single operation.

The Three Lifetimes In Plain Terms

Singleton means one instance for the entire application. It is great when you want a shared, stable resource or cache. It is dangerous if the object is not thread-safe or if it accidentally holds user-specific state.

Scoped means one instance per logical operation or request. It is ideal when multiple collaborators need to share the same state during a single workflow but you do not want that state to leak beyond it.

Transient means a fresh instance every time. It is useful for short-lived, lightweight objects or when you need a clean slate for each use.

Real-World Examples And Why They Fit

Singleton: configuration objects, in-memory caches, metrics registries, and thread-safe SDK clients. These are expensive to set up or are meant to be shared. Making them scoped or transient would waste resources and create duplicate caches with inconsistent data.

Scoped: unit-of-work services, request-scoped metrics, ORM contexts that track changes during a single request, and per-request correlation IDs. Making these singleton risks leaking one user’s state into another. Making them transient breaks the “shared state within a request” guarantee and can cause inconsistent transactions.

Transient: stateless computation services, validators, formatters, and mappers. These don’t need to be shared, and a fresh instance is cheap. Making them singleton can work if they are thread-safe, but transient keeps you honest about not storing state.

Decision Questions to Ask

  1. Does this need to hold state across the whole app? If yes, singleton.
  2. Does this need to be shared only within a single operation? If yes, scoped.
  3. Should each use be isolated from all others? If yes, transient.

Then I sanity-check: if it is singleton, is it thread-safe and free of per-user state? If it is scoped, do all dependencies live at least as long as the scope? If it is transient, is the object cheap to create?

Subtle But Important Constraints

Lifetimes should not flow “downhill.”

  • A long-lived object should not depend on a shorter-lived object.
  • For example, a singleton should not depend on a scoped object, because the scoped object might be disposed while the singleton still holds a reference.

Also, “stateless” does not automatically mean we should make it a singleton.

  • Stateless but expensive to construct (like a connection-heavy HTTP client) often belongs as a singleton.
  • Stateless and cheap (like a small validator) is fine as transient. The decision is about cost and safety, not just state.

Common Misconceptions

“Loggers must be singletons.” Not necessarily, but most logger implementations are designed to be thread-safe and shared, so singleton is usually a good fit.

“Database connections should be singleton.” Usually no. You generally want connection pooling under the hood, not a single long-lived connection. Many ORMs recommend a scoped context so you get a consistent unit of work per request.

“Transient is useless if it’s stateless.” It can still be useful as a guardrail. A transient service that is accidentally made stateful will fail fast instead of silently leaking state across requests.

Final Takeaway

These lifetimes are about scope of sharing, not about right or wrong. Pick the smallest lifetime that still satisfies your needs, then ensure the object’s thread-safety and dependencies match that lifetime. When in doubt, use the lifetime that matches your logical operation boundaries, not the one you saw in a blog post.

Is DI Applicable Everywhere?

Yes, the idea applies across frontend, backend, mobile, desktop, and CLI apps. Any time a component depends on another component, you can inject that dependency rather than create it directly. The difference is how you implement it: backend frameworks often provide DI containers, while frontend or mobile code may use simpler constructor injection, factories, or manual wiring.

  • Ask: “Who should share this instance?” If the answer is “the whole app,” use singleton. If the answer is “only this operation,” use scoped. If the answer is “nobody, each use should be independent,” use transient.

When unsure, start with the smallest lifetime that still makes sense for correctness, then relax it for performance if needed. A good stress test is: “If two users hit this at the same time, could they interfere?” If yes, do not use singleton.

This post is licensed under CC BY 4.0 by the author.