DEV Community

Dev Cookies
Dev Cookies

Posted on

Designing Clean, Scalable Microservices: Understanding Class Types in Low-Level Design

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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 or Coordinates.
  • 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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); }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)