Lifetimes are ownership decisions

A singleton lives for the application lifetime. A scoped service usually lives for one request. A transient service is created each time it is requested. Those are not trivia labels; they decide what can safely hold state.

Many production bugs come from putting request-specific state into long-lived services or making expensive objects transient without thinking about construction cost.

ts
builder.Services.AddScoped<IOrderWorkflow, OrderWorkflow>();
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();

DbContext is usually scoped for a reason

Entity Framework DbContext is designed around a unit of work. In web APIs, that usually maps well to a request scope. Passing DbContext through random helpers or storing it in long-lived objects creates confusing lifetime problems.

Try the idea

DI lifetime picker

Choose a service lifetime and read the production tradeoff.

scoped

One instance per request scope. Usually right for DbContext and request workflow services.

builder.Services.AddScoped<IInvoiceService, InvoiceService>();

Avoid service soup

A service with a vague name becomes a drawer for unrelated logic. Prefer names that describe the workflow: InvoiceApprovalService is clearer than CommonService.

Production notes for .NET APIs

Backend code becomes maintainable when HTTP boundaries, service boundaries, and persistence boundaries are named. A route that works once is not the same as an API that another developer can safely extend.

Before shipping, review lifetimes, validation behavior, request contracts, logging, and the exact error shape that frontend code will receive.

Conclusion

DI is not just how services are created. It is a map of ownership, lifetime, and responsibility.