4 The Spring context: Using abstractions
Spring applications are easier to build and maintain when objects depend on abstractions instead of concrete classes. An interface acts as a contract: it defines what behavior is needed, while each implementation decides how that behavior is carried out. This separation reduces coupling, so one object can change its internal implementation without forcing changes in the objects that use it.
The chapter illustrates this idea with a comment-publishing example. A service handles the use case by delegating to two separate responsibilities: storing the comment and sending a notification. By defining those responsibilities as interfaces and injecting implementations through the Spring context, the service stays independent from the specific database or messaging technology. If multiple implementations exist for the same interface, Spring needs help choosing one, which can be done by marking a default with a primary designation or by naming and selecting a specific implementation.
The chapter also explains how to decide which objects belong in the Spring context. Beans should be managed by Spring when they need dependency injection or when Spring must control and enhance them, but simple data objects do not need to be managed this way. Stereotype annotations are used to register classes as beans, and more specific ones such as service and repository make the role of each class clearer. Overall, the main message is that abstractions, clear responsibilities, and dependency injection work together to keep Spring applications flexible, readable, and easier to evolve.
The DeliveryDetailsPrinter object delegates the responsibility of sorting the delivery details by the delivery addresses to another object named SorterByAddress.
Because the two objects are strongly coupled, if you want to change the sorting responsibility, you also need to change the object using this responsibility. A better design would allow you to change the sorting responsibility without changing the object that uses the responsibility.
Using an interface to decouple the responsibilities. Instead of depending directly on an implementation, the DeliveryDetailsPrinter object depends on an interface (a contract). DeliveryDetailsPrinter can use any object implementing this interface instead of being stuck to a specific implementation.
The DeliveryDetailsPrinter object depends on the Sorter interface. You can change the implementation of the Sorter interface and avoid making more changes to the object using this responsibility (DeliveryDetailsPrinter).
The CommentService object implements the “publish comment” use case. To do this, it delegates to the responsibilities implemented by the CommentRepository and the CommentNotificationProxy objects.
The CommentService object depends on the abstractions provided by CommentRepository and CommentNotificationProxy interfaces. The classes DBCommentRepository and EmailCommentNotificationProxy further implement these interfaces. This design decouples the implementation of the “publish comment” use case from its dependencies and makes the application easier to change for future developments.
The project structure. We declare a separate package for each responsibility to make the structure of the project easy to read and understand.
The classes we’ll mark with the @Component stereotype annotation are shaded gray. When the context is loaded, Spring creates instances of these classes and adds them to its context.
Sometimes, in real-world scenarios, we have multiple implementations of the same interface. When using dependency injection on the interface, you need to instruct Spring which is the implementation it should inject.
Sometimes you use dependencies that already provide implementations for specific interfaces. When you need to have custom implementations of those interfaces, you can use @Primary to mark your implementation as a default for DI. This way, Spring knows to inject the implementation you define and not the one provided by the dependency.
If different objects need to use different implementations of the same contract, we can use @Qualifier to name them and tell Spring where and what it needs to inject.
We use the @Service and @Repository annotations to explicitly mark the responsibilities of the components in our class design. Where Spring doesn’t offer a specific annotation for that responsibility, we continue to use @Component.
Summary
- Decoupling means that classes do not depend directly on specific implementations. This makes the application easier to change, extend, and maintain.
- In Java, we usually use interfaces to decouple implementations. An interface defines a contract that other classes can implement.
- When we use dependency injection with an interface, Spring looks for a bean that implements that interface.
- We use stereotype annotations on classes that Spring should create as beans. We do not use stereotype annotations on interfaces.
- If Spring finds more than one bean for the same interface, we must tell it which one to inject. We can do this in two common ways:
- Use @Primary to mark one bean as the default.
- Use @Qualifier to choose a specific bean by name.
- We use @Service for classes that contain service logic and @Repository for classes that work with data. These annotations make the role of each class clearer than using only @Component.
Spring Start Here, Second Edition ebook for free