Why Interface‑Based Domain Models Boost Flexibility in Java Back‑End Design
This article explains how designing domain objects, services, and repositories as interfaces in Java enables seamless switching between different persistence technologies and deployment models, improving modularity, testability, and adaptability for micro‑service architectures.
Domain Interface Design
Instead of defining domain models only as classes, you can declare them as interface s. For example:
public interface User {
// ...
}
public class UserImpl implements User {
// ...
}Although this may seem unnecessary at first, it becomes valuable when you need to support multiple data sources such as JPA and MyBatis.
Repository Implementation
A typical JPA repository that implements a UserRepository interface looks like:
public class JpaUserRepository implements UserRepository {
@Override
public Optional<User> findById(String id) {
UserPO userPO = this.entityManager.find(UserPO.class, id);
return Optional.ofNullable(userPO).map(UserPO::toUser);
}
@Override
public User save(User user) {
UserPO userPO = this.entityManager.find(UserPO.class, user.getId());
userPO.setNickname(user.getNickname());
return this.entityManager.merge(userPO).toUser();
}
}Because User is an interface, converting between persistence objects ( UserPO) and domain objects ( User) is straightforward.
Factory Method for Interface Instances
The JpaUser.of() method creates a concrete implementation from an interface instance:
public class JpaUser extends UserSupport {
public static JpaUser of(User user) {
if (user instanceof JpaUser) {
return (JpaUser) user;
}
var target = new JpaUser();
BeanUtils.copyProperties(user, target);
return target;
}
}Handling Generic Repository Limitations
Spring Data repositories require concrete generic types, so an ElasticsearchRepository cannot be declared directly for User. A delegating repository solves this:
public class DelegatingElasticsearchUserRepository implements UserRepository {
private final ElasticsearchUserRepository elasticsearchUserRepository;
public DelegatingElasticsearchUserRepository(ElasticsearchUserRepository elasticsearchUserRepository) {
this.elasticsearchUserRepository = elasticsearchUserRepository;
}
@Override
public User create(String id) {
return new ElasticsearchUser(id);
}
@Override
public Optional<User> findById(String id) {
return CastUtils.cast(this.elasticsearchUserRepository.findById(id));
}
@Override
public User save(User user) {
return this.elasticsearchUserRepository.save(ElasticsearchUser.of(user));
}
}Associating Interfaces with Persistence
Persisted entities cannot have interface‑typed fields directly. Use JPA's targetEntity attribute to specify the concrete class:
public class JpaOrder implements Order {
@OneToMany(targetEntity = JpaOrderItem.class)
private List<OrderItem> items = new ArrayList<>();
}Supported annotations include @OneToMany, @OneToOne, @ManyToOne, and @ManyToMany. For frameworks without targetEntity, encapsulate the conversion, e.g., in an Elasticsearch implementation:
public class ElasticsearchOrder implements Order {
private List<ElasticsearchOrderItem> items = new ArrayList<>();
@Override
public void setItems(List<OrderItem> items) {
this.items = Objects.requireNonNullElseGet(items, ArrayList::new)
.stream()
.map(ElasticsearchOrderItem::of)
.collect(Collectors.toList());
}
}Testing Object Creation via Service
Instead of using new, obtain domain objects through a service method:
@Test
public void testCreateUser() {
User user = this.userService.createUser(null); // replaces new User()
user.setNickname("Nickname");
user.setGender(Gender.MALE);
this.userService.addUser(user);
}System‑Level Interface Design
To enable seamless switching between standalone, clustered, and micro‑service deployments, place domain interfaces in a dedicated API module and provide separate implementation modules (e.g., user-api, user-openfeign-client, user-rest-client). The caller depends only on the appropriate implementation module, keeping business code unchanged across environments.
Open‑Source Example: Mallfoundry
Mallfoundry is a fully open‑source, multi‑tenant e‑commerce platform built with Spring Boot. It follows DDD, interface‑based design, and can run as a standalone application, a cluster, or in the cloud.
Conclusion
Interface‑based domain modeling creates a uniform contract for business logic, simplifies data‑source switching, and supports flexible deployment strategies, though it demands higher expertise from architects and developers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITFLY8 Architecture Home
ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
