RizTech Academy logo
RizTech Academy
Patterns in SpringLesson 3 of 530 min

Service boundaries and when to add an abstraction

The layering lesson said business logic lives in the service layer. This lesson goes deeper into a question that genuinely divides Spring developers: how to structure that service layer as an application grows — how big a service should be, when to introduce an interface, and when an extra abstraction helps versus when it is ceremony. The honest answer is "less abstraction than you think, added when a real need appears" — but the reasoning matters.

What a service is, and keeping it cohesive

A service holds the business logic for a slice of the domain. The first structural decision is cohesion: a service should be about one thing. ParcelService handles parcel operations; HubService handles hubs; BillingService handles billing. When a service starts doing several unrelated jobs — parcels and billing and notifications — it has become a "god service", and it should be split along the domain seams. The signal, as with a fat constructor: if you cannot describe what a service does in one sentence without "and", it is doing too much.

Conversely, do not over-split into a service-per-method — a BookParcelService, a CancelParcelService, a DeliverParcelService each with one method is fragmentation that scatters related logic. Aim for services that own a coherent set of related operations on one part of the domain. Cohesion, not size, is the measure.

The interface question: do you need ParcelService + ParcelServiceImpl?

Here is the debate every Spring codebase has. A widespread convention is to define every service as an interface plus an implementation:

public interface ParcelService { Parcel book(...); }
public class ParcelServiceImpl implements ParcelService { ... }

The honest assessment: this is usually unnecessary ceremony. The interface earns its place when there is a real reason for it:

  • Multiple implementations — you genuinely have (or clearly will have) more than one implementation to swap between (a real payment gateway and a sandbox one). Then an interface is a Strategy, and it is right.
  • A published API/contract — the interface is a boundary others implement or you want to stabilise.

But defining an interface with exactly one implementation, named …Impl, "in case we need another one day" is speculative generality — it adds a file and an indirection for a flexibility you do not have and probably never will. Modern Spring does not need the interface for proxying (@Transactional works on concrete classes via CGLIB), and testing does not need it (you mock the concrete class fine). So the rule: a service can be a plain concrete class; add an interface only when you have a concrete reason (multiple implementations, a real contract) — not by default. The …Impl reflex is a habit worth dropping.

When to add an abstraction — the general principle

The interface question is one case of a broader judgement that runs through this whole module: add an abstraction when a real, present need justifies it — not in anticipation. The principle, often stated as "YAGNI" (You Aren't Gonna Need It):

  • Add it when the need is real and present — you have two implementations now; you are hitting a problem the abstraction solves. Then the abstraction pays for itself immediately.
  • Do not add it for an imagined future — "we might need to swap the database", "we might add another provider", "this might get complex". Most such futures never arrive, and the abstraction is pure cost (more code, more indirection, harder to follow) until they do. If the future does arrive, you refactor then — with the actual requirement in hand, you will design a better abstraction than you could guess now.

Speculative abstraction is the most common form of over-engineering in Spring codebases: interfaces with one implementation, generic "frameworks" for a single use case, configuration for flexibility no one asked for. Each feels responsible and is actually a bet against YAGNI that usually loses.

The service layer versus the entity: where logic sits

One more boundary, echoing the Django "fat models vs service layer" debate. In Spring with JPA, entities are typically kept relatively thin (data + simple derived accessors), and business logic goes in services — the opposite emphasis to Django's "fat models", partly because JPA entities are managed by the persistence context and mixing heavy behaviour into them complicates that. So Spring's idiom is: entities hold data and small domain methods; services hold the business operations and orchestration. A rich-domain (DDD-style) approach that puts more behaviour on entities is a legitimate alternative for complex domains, but the mainstream Spring default is a service layer holding the logic. Either way, the boundary is the same one the layering lesson drew — logic out of controllers and repositories, into a cohesive service — and the abstraction discipline is the same: structure for the complexity you have, not the complexity you imagine.

Check your work

Cohesion. A service is about one thing (parcels, hubs, billing); split a "god service" doing several unrelated jobs along domain seams; do not fragment into a service-per-method. Cohesion, not size, is the measure.

The interface question. Service + ServiceImpl is usually unnecessary ceremony. Add an interface only for a real reason — multiple implementations (a Strategy) or a published contract — not speculatively. Spring does not need it for proxying or testing. Drop the …Impl reflex.

When to abstract (YAGNI). Add an abstraction for a present, real need; do not add it for an imagined future (which usually never comes and costs indirection meanwhile) — refactor when the need actually arrives, with the real requirement in hand.

Where logic sits. Spring's idiom: thin entities (data + small methods), business logic in cohesive services (the opposite emphasis to Django's fat models, due to JPA-managed entities); rich-domain is a valid alternative for complex domains.

Practice

  1. Take a service doing several unrelated jobs and split it along domain seams; then take three single-method services and reason about whether they should merge.
  2. Find a Service/ServiceImpl pair with one implementation and argue whether the interface earns its place; remove it if not.
  3. Identify a genuine case for a service interface (two real implementations — e.g. real vs sandbox gateway) and implement it as a Strategy.
  4. Find a speculative abstraction ("in case we need…") and reason about the YAGNI cost of keeping it.
  5. Decide, for a piece of logic, whether it belongs on the entity (thin domain method) or in the service (business operation).
  6. Describe an abstraction you would add only once a specific future requirement became real — and why adding it now would be premature.

Official documentation

Next: do not fight the framework.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship