When we move from high-level design (HLD) to low-level design (LLD), the real engineering begins.
LLD is where you decide how every module, class, and method collaborates to deliver a robust system.
A clean, maintainable microservice starts with carefully defining class responsibilities.
In Java and Spring Boot applications, classes typically fall into a set of well-understood categories.
This post explores these categories—Entity, Manager/Service, Repository, Controller, DTO/VO, Utility, Factory/Builder, and Configuration—with best practices and examples you can apply immediately.
1️⃣ Entity / Domain Classes: The Heart of Your Model
Purpose
Entities represent your core business objects. They are the nouns of your domain—User
, Order
, Invoice
.
Characteristics
- Usually annotated with
@Entity
when using JPA/Hibernate. - Contain fields and relationships that mirror your database schema.
- Encapsulate domain logic (e.g.,
order.calculateTotal()
), but never handle persistence directly.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private List<OrderItem> items = new ArrayList<>();
public BigDecimal totalAmount() {
return items.stream()
.map(OrderItem::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Best Practices
- Follow Domain-Driven Design (DDD): rich domain models, meaningful invariants.
- Prefer immutable Value Objects (e.g.,
Money
,Address
) for small, self-contained concepts.
2️⃣ Manager / Service Classes: The Business Orchestrators
Purpose
Services (often called Managers in older codebases) implement use cases and coordinate multiple entities and external systems.
Key Points
- Annotated with
@Service
. - Define transactional boundaries using
@Transactional
. - Contain business workflows and validations.
@Service
public class OrderService {
private final OrderRepository repository;
private final PaymentGateway paymentGateway;
public OrderService(OrderRepository repository, PaymentGateway paymentGateway) {
this.repository = repository;
this.paymentGateway = paymentGateway;
}
@Transactional
public Order placeOrder(OrderRequest request) {
Order order = new Order(request.items());
paymentGateway.charge(order.totalAmount());
return repository.save(order);
}
}
Best Practices
- Adhere to the Single Responsibility Principle—each service should cover one cohesive capability.
- Avoid trivial CRUD methods here; delegate those to repositories.
3️⃣ Repository / DAO Classes: Persistence Gateways
Repositories abstract database access so that services never touch SQL or ORM details.
- Annotated with
@Repository
or created via Spring Data interfaces. - Expose simple, intention-revealing methods:
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerId(Long customerId);
}
4️⃣ Controller / Resource Classes: API Entry Points
Controllers expose your microservice to the outside world—REST, gRPC, or GraphQL.
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService service;
public OrderController(OrderService service) { this.service = service; }
@PostMapping
public ResponseEntity<OrderDto> place(@Valid @RequestBody OrderRequest request) {
Order order = service.placeOrder(request);
return ResponseEntity.ok(OrderDto.from(order));
}
}
Guidelines
- Controllers handle HTTP concerns only: request parsing, validation, and response formatting.
- Delegate business logic to services.
- Map exceptions to proper status codes with
@ControllerAdvice
.
5️⃣ DTO, VO, and Mappers: Clean Data Boundaries
-
DTO (Data Transfer Object): Represents payloads for REST or inter-service calls.
Example:
OrderRequest
,OrderResponseDto
. -
VO (Value Object): Immutable domain values like
Money
orCoordinates
. - Mapper: Converts between Entity and DTO. Tools like MapStruct automate this.
public record OrderDto(Long id, BigDecimal total) {
public static OrderDto from(Order order) {
return new OrderDto(order.getId(), order.totalAmount());
}
}
6️⃣ Utility / Helper Classes: Pure Reusability
Utility classes encapsulate stateless helper logic—date formatting, string sanitization, etc.
- Should be final with a private constructor.
- Contain only static methods.
public final class DateTimeUtils {
private DateTimeUtils() {}
public static LocalDate todayUtc() { return LocalDate.now(ZoneOffset.UTC); }
}
7️⃣ Factory and Builder Classes: Controlled Object Creation
Complex object creation belongs in a dedicated factory or builder.
-
Factory:
PaymentFactory.createPayment(...)
centralizes creation logic. - Builder: Useful for immutable objects with many optional fields.
Order order = Order.builder()
.customerId(123L)
.addItem(item1)
.addItem(item2)
.build();
8️⃣ Configuration & Infrastructure Classes: The Plumbing
These classes wire up cross-cutting concerns:
- Spring
@Configuration
for beans. - Security, caching, messaging (Kafka, RabbitMQ) configs.
- External service adapters:
EmailGateway
,PaymentClient
.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("orders");
}
}
Suggested Package Layout
A clean microservice often follows this structure:
com.example.orderservice
├─ controller // REST endpoints
├─ service // Business logic
├─ repository // JPA repositories
├─ domain/entity // Entities & Value Objects
├─ dto // Request/Response DTOs
├─ mapper // DTO ↔ Entity converters
├─ config // Spring Config classes
└─ util // Utility classes
This separation makes dependencies explicit and code easy to navigate.
Key Design Principles to Remember
- Single Responsibility Principle – each class changes for only one reason.
- Dependency Inversion – high-level modules depend on interfaces, not concrete classes.
- Domain-Driven Design – separate Domain, Application, and Infrastructure layers.
- Testability – keep classes small and focused so you can test them in isolation.
Wrapping Up
In LLD, clear class categorization is more than aesthetics—it’s about scalability, maintainability, and long-term velocity.
By respecting these class types and their responsibilities, you create microservices that are easy to extend, refactor, and test.
Whether you’re building a small internal API or a large distributed system, these principles will help you deliver clean, scalable, and maintainable Java/Spring Boot software with engineering precision.
Happy designing!
Top comments (0)