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.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
Why Interface‑Based Domain Models Boost Flexibility in Java Back‑End Design

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaspringDDDInterfaceRepository
ITFLY8 Architecture Home
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.