Challenge

Make the cycle impossible

20 minintermediate28 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • A cycle is an ordering impossibility, not a framework limitation
  • Field injection satisfies it only because wiring is a second pass
  • Converting to constructor injection surfaces the design problem
  • The fix is a third type, not a workaround or a configuration flag
  • allow-circular-references silences the message and keeps the problem

Starter

Starter.java
import java.lang.reflect.*;
import java.util.*;

/**
 * Challenge: this graph works. Make it impossible for it to have been written.
 *
 * ReportService and ArchiveService need each other, and field injection wires
 * them without complaint. Run it and see. Then convert both to constructor
 * injection, watch it fail, and fix the design rather than the wiring.
 *
 * The failure is the point. A cycle that a container can satisfy is still two
 * classes that cannot be built, understood or tested independently.
 */
public class Starter {

    static class TemplateEngine {
        String render(String id) {
            return "<report id=" + id + ">";
        }
    }

    static class ReportService {
        TemplateEngine templates;       // @Autowired
        ArchiveService archive;         // @Autowired

        String publish(String id) {
            String body = templates.render(id);
            archive.store(id, body);
            return body;
        }
    }

    static class ArchiveService {
        ReportService reports;          // @Autowired — and here is the cycle

        private final Map<String, String> stored = new LinkedHashMap<>();

        void store(String id, String body) {
            stored.put(id, body);
        }

        /** Re-renders a report that was never stored. */
        String restore(String id) {
            String body = stored.get(id);
            return body != null ? body : reports.publish(id);
        }
    }

    /** Field injection: instantiate everything, then wire. Two passes. */
    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);
                f.set(bean, beans.get(f.getType()));
            }
        }
        return beans;
    }

    // TODO 2: write the constructor-injection container.
    //
    //     static Object byConstructor(Class<?> type, LinkedHashSet<Class<?>> path)
    //
    // Resolve each constructor parameter by calling yourself. Track the path
    // so you can report the cycle instead of overflowing the stack — a
    // LinkedHashSet that refuses a duplicate gives you both the detection and
    // the chain to print.

    public static void main(String[] args) throws Exception {
        Map<Class<?>, Object> beans = byField(TemplateEngine.class, ReportService.class, ArchiveService.class);
        ReportService reports = (ReportService) beans.get(ReportService.class);
        ArchiveService archive = (ArchiveService) beans.get(ArchiveService.class);

        System.out.println("field injection wired the cycle: " + reports.publish("A1"));
        System.out.println("and it round-trips: " + archive.restore("A1"));
        System.out.println("archive.reports == reports ? " + (archive.reports == reports));

        // TODO 1: write down the order in which the two constructors would
        // have to run for constructor injection to succeed. Do it before
        // touching any code.

        // TODO 3: convert ReportService and ArchiveService to constructor
        // injection, with final fields, and build them with byConstructor.
        // It fails. Print the chain.

        // TODO 4: fix the design. Ask what job both classes are reaching for
        // when they call each other, extract exactly that, and let both depend
        // on it. Your container should then build the graph with no special
        // cases and no cycle.

        // TODO 5: two things people reach for instead. Explain why each is
        // worse than the fix you just made:
        //
        //   a) @Lazy on one of the two injection points
        //   b) spring.main.allow-circular-references=true
    }
}

Run it locally:

cd exercises/java/spring-core/constructor-vs-field-injection/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Write the order in which the two constructors would have to run. There isn't one — that is the answer, not a bug.

  2. Hint 2

    Which of the two classes actually needs the other, and for what? Name the one job they are both reaching for.

  3. Hint 3

    @Lazy breaks the cycle by injecting a proxy that resolves later. Ask yourself what that does to a startup-time failure.

  4. Hint 4

    If the extracted type has one method and no state, you have probably found the right seam.

Done when

  • The graph is fully constructor-injected and every field is final
  • No cycle remains — your container builds the graph without special cases
  • A comment names the third type you extracted and what it is responsible for
  • You can explain why @Lazy and allow-circular-references are not fixes

← Back to Why is constructor injection preferred over field injection?