ExerciseProduction incident
Production incident
The bean that was null for one endpoint
40 minintermediate2–10 yrs
A real incident: symptom first, cause hidden, tradeoff at the end.
The incident
A reporting service is field-injected throughout, which nobody has
questioned because it has always worked.
Then two things happen in the same week:
1. Someone adds a template warm-up call to the ReportService constructor,
so the first request after a deploy is not slow. The application now
fails to start, with a NullPointerException pointing at a field that is
annotated @Autowired and definitely has a bean.
2. A new endpoint restores an archived report. It works in the integration
test and throws in production, because ArchiveService needs
ReportService, ReportService needs ArchiveService, and which one is
usable depends on how the object was obtained.
What the team found:
1. The constructor runs before field injection. Nothing about the
annotation says when.
2. The two services cannot be constructed, understood or tested
independently, and nobody had noticed because the container hid it.
3. Adding @Lazy to one injection point made the application start again.
Fix both properly. The third finding is a trap, not a fix — say why.
What this teaches
- Field injection happens after construction, so a constructor sees nulls
- A dependency taken as a parameter cannot be null when the body runs
- A cycle means neither class can be used or tested on its own
- The fix for a cycle is a third type, extracted from what both were reaching for
- @Lazy and allow-circular-references hide the failure and keep the defect
Starter
Starter.java
import java.lang.reflect.*;
import java.util.*;
/**
* Production: the bean that was null for one endpoint.
*
* Spring is not on the classpath and does not need to be. Field injection is
* "instantiate everything, then assign the fields", which is what byField does
* below — so both defects reproduce exactly, with no container.
*
* Run it. It prints FAIL, four times over.
*/
public class Starter {
static class TemplateEngine {
boolean warmed = false;
int warmUp() {
warmed = true;
System.out.println(" templates warmed");
return 1;
}
String render(String id) {
return "<report id=" + id + ">";
}
}
static class ReportService {
TemplateEngine templates; // @Autowired
ArchiveService archive; // @Autowired
ReportService() {
// DEFECT 1: the constructor runs before field injection.
templates.warmUp();
}
String publish(String id) {
String body = templates.render(id);
archive.store(id, body);
return body;
}
}
static class ArchiveService {
ReportService reports; // @Autowired — DEFECT 2: the cycle
private final Map<String, String> stored = new LinkedHashMap<>();
void store(String id, String body) {
stored.put(id, body);
}
/** Re-publishes a report that was never archived. */
String restore(String id) {
String body = stored.get(id);
return body != null ? body : reports.publish(id);
}
}
/* ─────────────── two containers, for evidence ─────────────── */
/** Field injection: pass one instantiates, pass two assigns. */
static Map<Class<?>, Object> byField(Class<?>... types) throws Exception {
Map<Class<?>, Object> beans = new LinkedHashMap<>();
for (Class<?> t : types) {
beans.put(t, t.getDeclaredConstructor().newInstance());
}
for (Object bean : beans.values()) {
for (Field f : bean.getClass().getDeclaredFields()) {
if (Modifier.isFinal(f.getModifiers())) continue;
f.setAccessible(true);
if (beans.containsKey(f.getType())) f.set(bean, beans.get(f.getType()));
}
}
return beans;
}
/** Constructor injection: build the parameters first, or report the cycle. */
static Object byConstructor(Class<?> type, LinkedHashSet<Class<?>> path) throws Exception {
if (!path.add(type)) {
StringBuilder chain = new StringBuilder();
for (Class<?> c : path) chain.append(c.getSimpleName()).append(" → ");
throw new IllegalStateException("circular dependency: " + chain + type.getSimpleName());
}
Constructor<?> ctor = type.getDeclaredConstructors()[0];
Object[] args = new Object[ctor.getParameterCount()];
for (int i = 0; i < args.length; i++) {
args[i] = byConstructor(ctor.getParameterTypes()[i], path);
}
path.remove(type);
ctor.setAccessible(true);
return ctor.newInstance(args);
}
static final Class<?>[] SERVICES = { ReportService.class, ArchiveService.class };
public static void main(String[] args) {
boolean ok = true;
System.out.println("── startup ──");
TemplateEngine engine = new TemplateEngine();
Object built = null;
try {
Map<Class<?>, Object> beans =
byField(TemplateEngine.class, ReportService.class, ArchiveService.class);
engine = (TemplateEngine) beans.get(TemplateEngine.class);
built = beans.get(ReportService.class);
System.out.println(" context started");
} catch (Throwable t) {
System.out.println(" startup failed: " + rootCause(t));
}
ok &= check("the application starts and the templates are warmed",
built != null && engine.warmed);
System.out.println();
System.out.println("── restoring a report with no ReportService in existence ──");
boolean restored = false;
try {
ArchiveService archive = new ArchiveService();
System.out.println(" " + archive.restore("R-1"));
restored = true;
} catch (Throwable t) {
System.out.println(" " + rootCause(t));
}
ok &= check("ArchiveService is usable on its own", restored);
ok &= check("every field on every service is final", allFieldsFinal(SERVICES));
boolean declaresAndAcyclic;
try {
declaresAndAcyclic = declaresDependencies(SERVICES) && acyclic(SERVICES);
} catch (IllegalStateException e) {
System.out.println(" " + e.getMessage());
declaresAndAcyclic = false;
}
ok &= check("dependencies are declared in constructors, and the graph is acyclic",
declaresAndAcyclic);
System.out.println();
System.out.println(ok ? "PASS" : "FAIL");
}
/* ─────────────── structural assertions ─────────────── */
static boolean allFieldsFinal(Class<?>[] types) {
for (Class<?> t : types) {
for (Field f : t.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers()) && !Modifier.isFinal(f.getModifiers())) {
return false;
}
}
}
return true;
}
static boolean declaresDependencies(Class<?>[] types) {
for (Class<?> t : types) {
if (t.getDeclaredConstructors()[0].getParameterCount() == 0) return false;
}
return true;
}
static boolean acyclic(Class<?>[] types) {
for (Class<?> t : types) walk(t, new LinkedHashSet<>());
return true;
}
static void walk(Class<?> type, LinkedHashSet<Class<?>> path) {
if (!path.add(type)) {
StringBuilder chain = new StringBuilder();
for (Class<?> c : path) chain.append(c.getSimpleName()).append(" → ");
throw new IllegalStateException("circular dependency: " + chain + type.getSimpleName());
}
for (Class<?> p : type.getDeclaredConstructors()[0].getParameterTypes()) walk(p, path);
path.remove(type);
}
static String rootCause(Throwable t) {
while (t.getCause() != null) t = t.getCause();
return t.getClass().getSimpleName() + ": " + t.getMessage();
}
static boolean check(String what, boolean passed) {
System.out.println((passed ? " ok " : " FAIL ") + what);
return passed;
}
}Run it locally:
cd exercises/java/spring-core/constructor-vs-field-injection/03-production
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Read the constructor and ask what has been assigned by the time its first line runs.
Hint 2
Try to build ArchiveService on its own, with no ReportService anywhere. What stops you?
Hint 3
Both classes call the other for the same underlying job. Name that job — it is the type you are missing.
Hint 4
Once every field is final, check whether the cycle is even expressible.
Done when
- The application starts, and the warm-up runs during startup
- ArchiveService can restore a report with no ReportService in existence
- Every field on every service is final
- Dependencies are declared in constructors and the graph is acyclic
- A comment says why @Lazy was not the fix
Solution
Show the solution — try it yourself first
Solution.java
import java.lang.reflect.*;
import java.util.*;
/**
* Solution: the bean that was null for one endpoint.
*
* Two defects, one root cause each, and neither is fixed by configuration.
*
* Defect 1 — the constructor used an injected field. Field injection assigns
* fields AFTER the object exists, so every @Autowired field is null while the
* constructor body runs. Taking the dependency as a parameter makes the bug
* unwritable: a parameter cannot be null before the body it was passed to.
*
* Defect 2 — ReportService and ArchiveService needed each other. Field
* injection satisfied that in its second pass, which is the only reason it
* ever worked, and the price was that neither class could be built, read or
* tested on its own.
*
* Both services were calling the other for the same underlying job: turn an id
* into a rendered report. That job is the missing type. Extract ReportRenderer,
* let both depend on it, and the cycle is not fixed — it is no longer
* expressible.
*
* Why @Lazy was not the fix: it injects a proxy that resolves the real bean on
* first use, so startup succeeds and the cycle survives. It converts a
* startup-time failure into a runtime one and leaves two classes that still
* cannot be understood separately. Same objection to
* spring.main.allow-circular-references=true, which only silences the message.
*/
public class Solution {
static class TemplateEngine {
boolean warmed = false;
int warmUp() {
warmed = true;
System.out.println(" templates warmed");
return 1;
}
String render(String id) {
return "<report id=" + id + ">";
}
}
/** The job both services were reaching for, and nothing else. */
static class ReportRenderer {
private final TemplateEngine templates;
private final int warmedCount;
ReportRenderer(TemplateEngine templates) {
this.templates = templates;
// FIX 1: a parameter, so it cannot be null here.
this.warmedCount = templates.warmUp();
}
String render(String id) {
return templates.render(id);
}
int warmedCount() {
return warmedCount;
}
}
/** FIX 2: depends on the renderer, not on ReportService. */
static class ArchiveService {
private final ReportRenderer renderer;
private final Map<String, String> stored = new LinkedHashMap<>();
ArchiveService(ReportRenderer renderer) {
this.renderer = renderer;
}
void store(String id, String body) {
stored.put(id, body);
}
String restore(String id) {
String body = stored.get(id);
return body != null ? body : renderer.render(id);
}
}
static class ReportService {
private final ReportRenderer renderer;
private final ArchiveService archive;
ReportService(ReportRenderer renderer, ArchiveService archive) {
this.renderer = renderer;
this.archive = archive;
}
String publish(String id) {
String body = renderer.render(id);
archive.store(id, body);
return body;
}
}
/** Constructor injection: build the parameters first, or report the cycle. */
static Object byConstructor(Class<?> type, LinkedHashSet<Class<?>> path,
Map<Class<?>, Object> singletons) throws Exception {
if (singletons.containsKey(type)) return singletons.get(type);
if (!path.add(type)) {
StringBuilder chain = new StringBuilder();
for (Class<?> c : path) chain.append(c.getSimpleName()).append(" → ");
throw new IllegalStateException("circular dependency: " + chain + type.getSimpleName());
}
Constructor<?> ctor = type.getDeclaredConstructors()[0];
Object[] args = new Object[ctor.getParameterCount()];
for (int i = 0; i < args.length; i++) {
args[i] = byConstructor(ctor.getParameterTypes()[i], path, singletons);
}
path.remove(type);
ctor.setAccessible(true);
Object bean = ctor.newInstance(args);
singletons.put(type, bean);
return bean;
}
static final Class<?>[] SERVICES = { ReportService.class, ArchiveService.class, ReportRenderer.class };
public static void main(String[] args) {
boolean ok = true;
System.out.println("── startup ──");
TemplateEngine engine = null;
Object built = null;
try {
Map<Class<?>, Object> singletons = new LinkedHashMap<>();
built = byConstructor(ReportService.class, new LinkedHashSet<>(), singletons);
engine = (TemplateEngine) singletons.get(TemplateEngine.class);
System.out.println(" context started");
} catch (Throwable t) {
System.out.println(" startup failed: " + rootCause(t));
}
ok &= check("the application starts and the templates are warmed",
built != null && engine != null && engine.warmed);
System.out.println();
System.out.println("── restoring a report with no ReportService in existence ──");
boolean restored = false;
try {
ArchiveService archive = new ArchiveService(new ReportRenderer(new TemplateEngine()));
System.out.println(" " + archive.restore("R-1"));
restored = true;
} catch (Throwable t) {
System.out.println(" " + rootCause(t));
}
ok &= check("ArchiveService is usable on its own", restored);
ok &= check("every field on every service is final", allFieldsFinal(SERVICES));
boolean declaresAndAcyclic;
try {
declaresAndAcyclic = declaresDependencies(SERVICES) && acyclic(SERVICES);
} catch (IllegalStateException e) {
System.out.println(" " + e.getMessage());
declaresAndAcyclic = false;
}
ok &= check("dependencies are declared in constructors, and the graph is acyclic",
declaresAndAcyclic);
System.out.println();
System.out.println(ok ? "PASS" : "FAIL");
}
/* ─────────────── structural assertions ─────────────── */
static boolean allFieldsFinal(Class<?>[] types) {
for (Class<?> t : types) {
for (Field f : t.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers()) && !Modifier.isFinal(f.getModifiers())) {
return false;
}
}
}
return true;
}
static boolean declaresDependencies(Class<?>[] types) {
for (Class<?> t : types) {
if (t.getDeclaredConstructors()[0].getParameterCount() == 0) return false;
}
return true;
}
static boolean acyclic(Class<?>[] types) {
for (Class<?> t : types) walk(t, new LinkedHashSet<>());
return true;
}
static void walk(Class<?> type, LinkedHashSet<Class<?>> path) {
if (!path.add(type)) {
StringBuilder chain = new StringBuilder();
for (Class<?> c : path) chain.append(c.getSimpleName()).append(" → ");
throw new IllegalStateException("circular dependency: " + chain + type.getSimpleName());
}
for (Class<?> p : type.getDeclaredConstructors()[0].getParameterTypes()) walk(p, path);
path.remove(type);
}
static String rootCause(Throwable t) {
while (t.getCause() != null) t = t.getCause();
return t.getClass().getSimpleName() + ": " + t.getMessage();
}
static boolean check(String what, boolean passed) {
System.out.println((passed ? " ok " : " FAIL ") + what);
return passed;
}
}Stretch
The warm-up now runs inside a constructor, which means it runs before the
bean is fully published and blocks startup. Argue for moving it to
@PostConstruct or an ApplicationReadyEvent instead, and say what each choice
changes about failure handling and startup time.
← Back to Why is constructor injection preferred over field injection?