Spring Boot
Java
Dependency Injection
@Service Annotation
Application Design

Spring boot - Service class calling another Service class

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Having one Spring service call another is normal and often desirable. The service layer exists to hold business rules, and real workflows usually combine multiple business capabilities such as billing, inventory, notifications, or auditing.

The important question is not whether a service may call another service, but how the dependency is wired and where the responsibility boundary lives. Good service composition keeps controllers thin, makes code easier to test, and avoids duplicated logic.

Why Service-to-Service Calls Are Common

A controller should usually translate HTTP input into an application request and then hand work off to the service layer. Once the request reaches the service layer, it is common for one service to delegate part of the workflow to another specialized service.

An order flow is a simple example. OrderService may validate the purchase, ask PaymentService to authorize a charge, then store the result through a repository. That is cleaner than placing payment logic directly inside the controller or duplicating it across several services.

The key idea is cohesion. Each service should own one area of business logic:

  • 'OrderService coordinates order placement.'
  • 'PaymentService knows how to authorize or capture payments.'
  • 'EmailService sends notifications.'

When responsibilities are separated this way, each class stays focused and reusable.

Wiring Services with Constructor Injection

The recommended pattern is constructor injection. Spring creates both beans and injects the dependency automatically.

java
1package com.example.store.service;
2
3import java.math.BigDecimal;
4import org.springframework.stereotype.Service;
5
6@Service
7public class PaymentService {
8
9    public PaymentReceipt authorize(String customerId, BigDecimal amount) {
10        if (amount.signum() <= 0) {
11            throw new IllegalArgumentException("Amount must be positive");
12        }
13
14        return new PaymentReceipt(customerId, amount, true);
15    }
16}
java
1package com.example.store.service;
2
3import java.math.BigDecimal;
4import org.springframework.stereotype.Service;
5
6@Service
7public class OrderService {
8    private final PaymentService paymentService;
9
10    public OrderService(PaymentService paymentService) {
11        this.paymentService = paymentService;
12    }
13
14    public OrderConfirmation placeOrder(String customerId, BigDecimal total) {
15        PaymentReceipt receipt = paymentService.authorize(customerId, total);
16        return new OrderConfirmation(customerId, receipt.approved());
17    }
18}

This works because both classes are Spring-managed beans. You do not call new PaymentService() yourself. If you instantiate the dependency manually, Spring cannot apply bean lifecycle management, configuration, proxies, or transaction handling.

Constructor injection also improves tests because the dependency is explicit. You can supply a mock or fake implementation without spinning up the whole container.

java
1package com.example.store.service;
2
3import static org.junit.jupiter.api.Assertions.assertTrue;
4import static org.mockito.Mockito.when;
5
6import java.math.BigDecimal;
7import org.junit.jupiter.api.Test;
8import org.junit.jupiter.api.extension.ExtendWith;
9import org.mockito.InjectMocks;
10import org.mockito.Mock;
11import org.mockito.junit.jupiter.MockitoExtension;
12
13@ExtendWith(MockitoExtension.class)
14class OrderServiceTest {
15
16    @Mock
17    private PaymentService paymentService;
18
19    @InjectMocks
20    private OrderService orderService;
21
22    @Test
23    void placesOrderWhenPaymentIsApproved() {
24        when(paymentService.authorize("cust-7", new BigDecimal("29.99")))
25            .thenReturn(new PaymentReceipt("cust-7", new BigDecimal("29.99"), true));
26
27        OrderConfirmation confirmation =
28            orderService.placeOrder("cust-7", new BigDecimal("29.99"));
29
30        assertTrue(confirmation.approved());
31    }
32}

Designing Clear Boundaries

Calling another service should represent a real dependency in the domain. If OrderService needs payment authorization, a call to PaymentService makes sense. If two services are constantly calling each other, the design is usually signaling a missing abstraction.

For example, if OrderService and InvoiceService both contain overlapping billing steps, that shared logic probably belongs in a dedicated BillingService. Extracting the common behavior reduces duplication and prevents circular dependencies.

It is also worth deciding whether a service should orchestrate a workflow or implement a single rule. Coordination logic belongs in an application service, while low-level business rules often belong in smaller specialized services.

Common Pitfalls

  • Creating dependencies with new instead of injection. That bypasses Spring and breaks features like proxy-based transactions.
  • Using field injection everywhere. Constructor injection is easier to test and makes required dependencies obvious.
  • Accidentally creating circular dependencies such as A -> B -> A. If that happens, move the shared behavior into a third service.
  • Assuming every call shares the same transaction behavior. @Transactional works through Spring proxies, so self-invocation inside the same class behaves differently from a call that crosses bean boundaries.
  • Letting controllers grow business logic because a service call feels inconvenient. That usually leads to duplicated rules and harder testing.

Summary

  • One Spring service calling another is a standard pattern when responsibilities are well separated.
  • Prefer constructor injection so Spring can manage the dependency correctly.
  • Keep services cohesive: orchestration in one place, specialized rules in another.
  • Use tests with mocks to verify service collaboration without starting the full application.
  • If service calls become tangled, refactor toward clearer boundaries instead of adding more coupling.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.