Foundation · Understand
Production Implementation: Inspectable Spring Wiring
Assemble a real Controller -> Service -> Repository slice with constructor injection, @Configuration beans, and lifecycle hooks you can observe at startup.
Primary source: Spring Framework 6.2 Reference: The IoC Container
Lesson 14 of 14
1d streakThe Architectural Question
Tutorial wiring hides the decisions production code must make: which dependencies are required, where interfaces earn their keep, and what the container actually does at startup.
What this guide proves
- Wire collaborators with constructors instead of field injection
- Register third-party types through @Configuration and @Bean
- Observe bean creation order, proxy wrapping, and shutdown callbacks
Capstone: compose one notification use case
This lesson is an integration exercise, not a second introduction to IoC. Use the earlier lessons as reference: @Service and @Bean provide definitions, constructor injection resolves collaborators, scopes define identity, lifecycle callbacks define readiness, and proxies apply cross-cutting behavior. Here you assemble those decisions into one small notification feature and verify the boundaries that matter in production.
- 01Choose the application boundary: NotificationService owns the use case and MessageSender is its port.
- 02Choose the registration boundary: component scanning finds the service while Java configuration supplies the adapter.
- 03Choose the runtime boundary: the service is a singleton, its dependency is constructor-injected, and any advised call enters through the managed reference.
- 04Choose the proof: test the use case without Spring, then start a focused context and verify the intended beans and collaborators exist.
Use the earlier lessons, do not reread them here
Implement the composition root
Apply the module rules to a production-shaped boundary. The service owns the use-case behavior; the composition root chooses the MessageSender implementation. The service does not construct infrastructure, and its required collaborator is an immutable constructor dependency.
package com.example.core.notification;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class NotificationService {
private final MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = Objects.requireNonNull(sender, "sender must not be null");
}
public void sendWelcome(String recipient) {
sender.send("Welcome to the platform", recipient);
}
}Fail-fast assembly
Break it deliberately: The bypassed proxy test verification
Verify in a real test that direct instantiation bypasses the CGLIB proxy while container injection preserves it:
@SpringBootTest
class ProxyVerificationTest {
@Autowired
private NotificationService containerManagedService;
@Test
void testProxyIntegrity() {
// 1. Container-managed service has collaborators wired
assertThat(containerManagedService).isNotNull();
// 2. Direct instantiation bypasses container
NotificationService unmanaged = new NotificationService(null);
assertThatThrownBy(() -> unmanaged.sendWelcome("user@example.com"))
.isInstanceOf(NullPointerException.class);
}
}Check your understanding
Active Recall Checkpoint
What happens when a method annotated with @Transactional is called from another method within the same bean (self-invocation)?
Hint: Remember which object holds the interceptor advice.
Show answer & explanation
The self-invocation calls the method on this directly, completely bypassing the dynamic proxy. Consequently, transaction advice never intercepts the call, and no transaction is opened or committed.
Working Implementation
Code Walkthrough
Build a notification pipeline where swapping one @Bean changes the delivery adapter without touching any consumer.
package com.example.core;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@SpringBootApplication
public class InspectableWiringProductionLab {
private static final Logger log = LoggerFactory.getLogger(InspectableWiringProductionLab.class);
public interface TaskAuditClient {
void logAction(String action);
}
@Service
public static class TaskService {
private final TaskAuditClient auditClient;
public TaskService(TaskAuditClient auditClient) {
this.auditClient = auditClient;
}
@PostConstruct
void init() {
log.info("TaskService initialized with auditClient: {}", auditClient.getClass().getSimpleName());
}
public void completeTask(long id) {
auditClient.logAction("Task " + id + " marked complete");
}
}
@Bean
TaskAuditClient taskAuditClient() {
return action -> log.info("[AUDIT] {}", action);
}
@Component
static class ProxyAuditProbe implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (AopUtils.isAopProxy(bean)) {
log.info("Proxy detected on bean '{}' (class: {})", beanName, bean.getClass().getName());
}
return bean;
}
}
@Bean
CommandLineRunner run(TaskService service) {
return args -> service.completeTask(42L);
}
public static void main(String[] args) {
SpringApplication.run(InspectableWiringProductionLab.class, args);
}
}Core Spring Implementation
Inspectable Core Spring wiring lab
Assemble one notification feature end to end: constructor-injected collaborators, visible bean lifecycle, proxy boundaries, and a documented circular-dependency redesign.
Component Boundaries
- 01The ApplicationContext instantiates and wires every collaborator; application classes never construct services with new.
- 02@Transactional and @Cacheable advice attach through CGLIB proxies built by BeanPostProcessors, so self-invocation skips advice.
- 03Dependency cycles fail at startup with BeanCurrentlyInCreationException; orchestration belongs in a single coordinator class.
Required Evidence
- A framework-free unit test proves NotificationService behavior with one fake MessageSender.
- The startup log separates proxied beans, such as transactional services, from raw instances.
- A failing-cycle test case records the original exception message and the coordinator refactor that replaced the cycle.
Implementation sequence
Step-by-Step Integration
Follow these structured steps to inject Core Spring functionality cleanly across your boundaries.
Expose dependencies through constructors
NotificationService.java
One final-field constructor fails fast at startup with UnsatisfiedDependencyException when a bean is missing, and lets unit tests substitute fakes without booting Spring.
@Service
class NotificationService {
private final MessageSender sender;
NotificationService(MessageSender sender) {
this.sender = sender;
}
void sendWelcome(User user) {
sender.send("welcome", user.email());
}
}Verify Configuration
NotificationServiceTest constructs the service with a fake MessageSender and asserts sendWelcome dispatches to user.email() with no ApplicationContext started.
Log proxy wrapping during startup
LifecycleProbe.java
Beans wrapped for transactions or caching become CGLIB subclasses; calling methods via this bypasses that proxy and silently drops the advice.
@Component
class LifecycleProbe implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String name) {
if (AopUtils.isAopProxy(bean)) {
log.info("{} is proxied", name);
}
return bean;
}
}Verify Configuration
Startup output contains one INFO line per proxied bean, for example orderService is proxied, while non-advised components print no marker.
Break the circular dependency by design
NotificationCoordinator.java
Adding @Lazy or switching to field injection silences BeanCurrentlyInCreationException without fixing the tangled responsibilities that produced the cycle.
@Service
class NotificationCoordinator {
private final UserRegistration registration;
private final NotificationService notifications;
void register(RegisterUser command) {
User user = registration.create(command);
notifications.sendWelcome(user);
}
}Verify Configuration
Introducing a two-way constructor dependency reproduces the BeanCurrentlyInCreationException in CI, and the coordinator refactor brings contextLoads() back to green with no cycle warnings.
Synthesis & Production Verification
Production Acceptance Criteria
- Every required collaborator arrives through an immutable constructor parameter
- A third-party client class is registered via @Bean with its configuration in one place
- Startup logs show instantiation order and @PreDestroy runs on shutdown
Key Takeaway
The Core Spring capstone composes a small notification use case from explicit ports, managed components, constructor dependencies, and focused verification.
Architectural & Technical Review
Core Interview Questions
Q01What should the Core Spring capstone verify after the application context starts?Show answer
It should verify that the intended service and adapter are registered, the service has its required collaborator, and the use case can be exercised through the managed reference. Proxy and post-processor mechanics belong to their dedicated lessons.
Q02Why should the capstone use a port for MessageSender instead of a concrete provider?Show answer
The port keeps the use case independent of infrastructure and lets a unit test provide a fake sender while the composition root selects the production adapter.
Q03Which evidence separates a plain unit test from a Spring context test in this capstone?Show answer
The unit test constructs NotificationService with a fake MessageSender and proves business behavior without Spring. The context test proves registration and wiring. Keeping those proofs separate shows whether a failure belongs to application behavior or container assembly.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Treating the capstone as a new definition lesson — The capstone should apply the module concepts and expose integration mistakes. Return to the dedicated proxy or circular-dependency lesson when that mechanism is the subject of the question.
- Pitfall: Putting provider choices inside the use case — Constructing an SMTP, SMS, or push client inside NotificationService couples policy to infrastructure. Keep the provider selection in configuration and inject the MessageSender port.
Module Certification Quiz
Prove Module Mastery
Comprehensive question set covering all lessons in this module.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources