@Service
A @Component specialisation for classes that hold business logic: the layer between controllers and repositories.
@Service | @Service("beanName")- Added in Spring Boot Spring Framework 2.5
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | String= "" | Explicit bean name. When empty, the simple class name with a lower-case first letter is used. |
Return value
n/aRegisters the class as a bean through component scanning and labels it as part of the service layer. It adds no behaviour of its own beyond @Component.
@Service is @Component with a name that states a role. Spring scans and creates the bean exactly as it does for @Component; nothing extra happens at runtime. The value is in reading the code: a class marked @Service holds a use case, such as placing an order or issuing a refund, and coordinates repositories, clients and other services to carry it out.
What belongs in a service
Controllers should translate HTTP into method calls and back: read the path and body, call one service method, map the result to a response. Repositories should load and store data. Everything in between belongs in services: rules ("an order over 10,000 needs approval"), calculations, calls to other systems, and the decision about what counts as one unit of work.
That last point is why @Transactional usually goes on service methods. A use case that saves an order and decrements stock must commit both or neither, and only the service knows that both writes belong together. Put the annotation on public methods of the service bean; Spring applies it through a proxy wrapped round the bean.
The self-invocation trap
Because transactions, caching and retries are applied by a proxy, they only run when a call comes in from outside the bean. A method calling another @Transactional method on this bypasses the proxy, so the inner annotation is ignored. The fix is structural: move the inner method to a separate bean, or make the outer method the transactional one.
Keep services plain
A service with constructor injection is an ordinary Java object. You can test its rules with new and hand-written fakes or Mockito mocks, in milliseconds, without starting Spring. If a service needs the HttpServletRequest, a ResponseEntity or web exceptions, web concerns have leaked down a layer: pass plain values in and return domain objects or records.
Avoid one huge OrderService that collects every operation over time. Split by use case or sub-domain when a class grows past a few hundred lines or needs more than five or six collaborators; that is a sign it has more than one job.
Example
import java.math.BigDecimal;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
(name = "orders")
class Order {
Long id;
String customerEmail;
BigDecimal total;
String status;
protected Order() {
}
Order(String customerEmail, BigDecimal total) {
this.customerEmail = customerEmail;
this.total = total;
this.status = total.compareTo(new BigDecimal("10000")) > 0 ? "PENDING_APPROVAL" : "CONFIRMED";
}
}
interface OrderRepository extends JpaRepository<Order, Long> {
}
record PlaceOrder(String customerEmail, BigDecimal total) {
}
public class OrderService {
private final OrderRepository orders;
public OrderService(OrderRepository orders) {
this.orders = orders;
}
// One use case, one transaction.
public Long place(PlaceOrder command) {
if (command.total().signum() <= 0) {
throw new IllegalArgumentException("Total must be positive");
}
return orders.save(new Order(command.customerEmail(), command.total())).id;
}
}When to use it
- Placing an order that validates rules and saves several rows in one transaction
- A refund use case shared by a REST endpoint and a nightly batch job
- Business rules covered by fast unit tests that never start a Spring context
Frequently asked questions
Does @Service make my class transactional?
No. @Service adds no behaviour. Add @Transactional to the methods (or the class) that need a transaction.
Should services implement an interface?
Only when there will really be more than one implementation. Spring proxies classes directly, and Mockito can mock classes, so an interface per service is not required.
More examples
A unit test with no Spring context
The annotation does not get in the way of plain tests: the class is constructed with new and its rules are checked directly. Keep context-starting tests for wiring, not for business rules.
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.junit.jupiter.api.Test;
import org.springframework.stereotype.Service;
@Service
class DiscountService {
BigDecimal apply(BigDecimal total, int percent) {
if (percent < 0 || percent > 50) {
throw new IllegalArgumentException("Discount must be between 0 and 50");
}
BigDecimal factor = BigDecimal.valueOf(100 - percent).movePointLeft(2);
return total.multiply(factor).setScale(2, RoundingMode.HALF_UP);
}
}
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void appliesPercentage() {
assertThat(service.apply(new BigDecimal("80.00"), 25)).isEqualByComparingTo("60.00");
}
@Test
void rejectsLargeDiscounts() {
assertThatThrownBy(() -> service.apply(BigDecimal.TEN, 60))
.isInstanceOf(IllegalArgumentException.class);
}
}Fixing self-invocation by splitting the bean
If record() lived in AccountService and was called as this.record(...), REQUIRES_NEW would be ignored and the audit row would roll back with the main transaction. Moving it to its own bean sends the call through a proxy.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
class AuditWriter {
// Called from another bean, so the proxy applies REQUIRES_NEW.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(String event) {
// insert into audit_log ...
}
}
@Service
public class AccountService {
private final AuditWriter audit;
public AccountService(AuditWriter audit) {
this.audit = audit;
}
@Transactional
public void close(long accountId) {
// update accounts set status = 'CLOSED' ...
audit.record("account " + accountId + " closed");
}
}Controller depending on the service, not the repository
The controller only handles HTTP. The rules for when a refund is allowed live in the service, so a scheduled job or a message listener can call the same method later.
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@Service
class RefundService {
String refund(long orderId) {
// check the order can be refunded, call the payment provider, store the refund
return "RF-" + orderId;
}
}
@RestController
public class RefundController {
private final RefundService refunds;
public RefundController(RefundService refunds) {
this.refunds = refunds;
}
@PostMapping("/orders/{id}/refund")
public ResponseEntity<String> refund(@PathVariable long id) {
return ResponseEntity.accepted().body(refunds.refund(id));
}
}
Discussion