SOLID, Dependency Injection and Spring Boot Basics for Java Interviews
Codevity Admin · 10/25/2026
Step 30 of 38 · Java Interview Preparation for Beginners · Stage 6: Databases, Spring and testing
Codevity Admin · 10/25/2026
Two topics follow fresher-level Java in most job descriptions: design principles and Spring Boot. This article explains the ideas in plain Java first, because Spring is just a framework that automates them.
| Letter | Principle | In practice |
|---|---|---|
| S | Single Responsibility | A class has one reason to change. OrderService does not also format PDFs |
| O | Open/Closed | Add behaviour by adding new classes, not by editing a growing if/else or switch |
| L | Liskov Substitution | A subclass must be usable wherever its parent is, without surprises (a Square that breaks a Rectangle's width/height contract violates it) |
| I | Interface Segregation | Many small interfaces beat one large one that forces empty methods |
| D | Dependency Inversion | Depend on abstractions (interfaces), not on concrete classes |
A class should not create its own collaborators with new. It should receive them. That keeps it loosely coupled and easy to test, because a test can pass in a fake.
import java.util.*;
public class DependencyInjection {
interface MessageSender {
void send(String to, String text);
}
static class EmailSender implements MessageSender {
public void send(String to, String text) {
System.out.println("EMAIL to " + to + ": " + text);
}
}
static class FakeSender implements MessageSender { // a hand-written test double
final List<String> sent = new ArrayList<>();
public void send(String to, String text) {
sent.add(to + "|" + text);
}
}
static class OrderService {
private final MessageSender sender; // depends on the abstraction
OrderService(MessageSender sender) { // constructor injection
this.sender = sender;
}
void placeOrder(String customer) {
sender.send(customer, "Your order is confirmed");
}
}
public static void main(String[] args) {
new OrderService(new EmailSender()).placeOrder("asha@example.com");
FakeSender fake = new FakeSender();
new OrderService(fake).placeOrder("ben@example.com");
System.out.println(fake.sent); // [ben@example.com|Your order is confirmed]
}
}OrderService neither knows nor cares whether it is talking to email, SMS or a fake. In a real application, a DI container (Spring) does the wiring that main does here.
| Injection style | Notes |
|---|---|
| Constructor | Preferred. Fields can be final, dependencies are explicit, and the object is never half-built |
| Setter | For optional dependencies |
Field (@Autowired on a field) | Short but hides dependencies and makes tests harder. Avoid it |
import java.util.*;
import java.util.function.*;
public class Patterns {
enum Registry { // Singleton: the JVM guarantees a single enum instance
INSTANCE;
private int counter;
int next() {
return ++counter;
}
}
static class Pizza { // Builder: readable construction of an object with many options
private final String size;
private final List<String> toppings;
private Pizza(Builder builder) {
this.size = builder.size;
this.toppings = List.copyOf(builder.toppings);
}
static Builder builder() {
return new Builder();
}
static class Builder {
private String size = "medium";
private final List<String> toppings = new ArrayList<>();
Builder size(String size) { this.size = size; return this; }
Builder topping(String topping) { toppings.add(topping); return this; }
Pizza build() { return new Pizza(this); }
}
@Override
public String toString() {
return size + " pizza with " + toppings;
}
}
public static void main(String[] args) {
System.out.println(Registry.INSTANCE.next()); // 1
System.out.println(Registry.INSTANCE.next()); // 2
Pizza pizza = Pizza.builder().size("large").topping("olives").topping("corn").build();
System.out.println(pizza); // large pizza with [olives, corn]
// Strategy: choose the algorithm at run time, here as lambdas in a map
Map<String, DoubleUnaryOperator> discounts = Map.of(
"none", price -> price,
"festival", price -> price * 0.9,
"clearance", price -> price * 0.5);
System.out.println(discounts.get("clearance").applyAsDouble(200)); // 100.0
}
}| Pattern | Idea | Seen in |
|---|---|---|
| Singleton | One instance only | Spring beans (one per container by default), Runtime.getRuntime() |
| Factory | A method decides which class to create | List.of, Calendar.getInstance() |
| Builder | Step-by-step construction | StringBuilder, Lombok @Builder |
| Strategy | Swap algorithms | |
| Observer | Notify listeners of events | Event listeners, |
| Decorator | Wrap an object to add behaviour | around |
For a classic singleton class (not an enum), the constructor is private, the instance is created once, and thread safety needs care. The enum singleton above, or a holder class, avoids double-checked-locking mistakes.
Spring Boot sets up a Spring application with sensible defaults. It is an opinionated layer on top of Spring: starters (one dependency pulls in a compatible set of libraries), auto-configuration (it configures beans based on what is on the classpath and in ) and an embedded server (Tomcat), so the whole application runs as . Spring Boot 3 requires Java 17 or later and uses packages instead of .
| Annotation | Meaning |
|---|---|
| A generic Spring-managed bean | |
| , , | Specializations of that express the layer. also translates persistence exceptions |
| Ask for a dependency. Optional on a single constructor | |
| A method in a class that produces a bean (useful for third-party classes) | |
| , | Read configuration |
| Run the method in a transaction. It rolls back on unchecked exceptions by default | |
| Load beans only in a given profile | |
| / | Choose between several beans of the same type |
Questions that come up again and again:
ComparatorPropertyChangeListenerBufferedReaderFileReaderapplication.propertiesjava -jar app.jarjakarta.*javax.*@Component@Service@Repository@Controller@Component@Repository@Autowired@Bean@Configuration@Value("${key}")@ConfigurationProperties@Transactional@Profile("dev")@Qualifier@Primarysingletonprototyperequestsession@TransactionalprivaterollbackFor@Controller@RestController@ResponseBodyapplication-dev.propertiesapplication-prod.propertiesPOSTSmsSenderDependencyInjectionPatterns// Illustrative: needs Spring Boot (spring-boot-starter-web and spring-boot-starter-data-jpa) on the classpath.
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}
@RestController // = @Controller + @ResponseBody: return values become JSON
@RequestMapping("/api/employees")
class EmployeeController {
private final EmployeeService service;
EmployeeController(EmployeeService service) { // one constructor: @Autowired is optional
this.service = service;
}
@GetMapping("/{id}")
ResponseEntity<Employee> get(@PathVariable Long id) {
return service.find(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()); // 404 when absent
}
@PostMapping
ResponseEntity<Employee> create(@Valid @RequestBody EmployeeRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(request));
}
}
@Service
class EmployeeService {
private final EmployeeRepository repository;
EmployeeService(EmployeeRepository repository) {
this.repository = repository;
}
Optional<Employee> find(Long id) {
return repository.findById(id);
}
@Transactional
Employee create(EmployeeRequest request) {
return repository.save(new Employee(request.name(), request.department()));
}
}
interface EmployeeRepository extends JpaRepository<Employee, Long> { // Spring Data writes the implementation
List<Employee> findByDepartment(String department); // a query derived from the method name
}