Overview

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.

FAQ

What is the main reason to use interfaces in Java design?Interfaces define a contract for what an object must do, while leaving the implementation details to the classes that implement them. This helps keep responsibilities clear and makes designs easier to maintain.
Why should objects depend on abstractions instead of concrete implementations?Depending on abstractions reduces coupling between objects. That way, you can change one implementation without having to change the classes that use it.
What does it mean that an interface defines the “what” and an implementation defines the “how”?The interface says what behavior is needed, such as sorting or storing data. The implementing class provides the actual steps used to carry out that behavior.
Why are abstractions often combined with dependency injection in Spring?Spring can supply the correct implementation at runtime while the dependent class only knows the interface. This makes the application more flexible and easier to change or test.
How does Spring choose an implementation when a class depends on an interface?Spring looks in its context for a bean whose class implements that interface and injects it. If more than one bean matches, you must guide Spring with additional hints.
Should interfaces be annotated with stereotype annotations like @Component?No. Interfaces cannot be instantiated, so adding stereotype annotations to them is useless and considered bad practice.
How do you decide which objects should be managed by the Spring context?Add objects to the Spring context if Spring needs to inject them into others or if they themselves need framework management. Objects that do not benefit from Spring, such as simple data objects, usually should not be added.
Why is a simple POJO like Comment usually not added to the Spring context?A POJO such as Comment is just data and does not need Spring’s dependency injection or other framework features. Managing it in the context would add unnecessary complexity.
What happens if multiple beans implement the same interface and Spring needs one of them?Spring may throw a NoUniqueBeanDefinitionException because it cannot decide which bean to inject. You then need to specify which implementation to use.
How can you tell Spring which implementation to inject when multiple beans match an interface?You can mark one implementation with @Primary to make it the default, or use @Qualifier to name a specific bean and request that exact one during injection.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Spring Start Here, Second Edition ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Spring Start Here, Second Edition ebook for free