Backend Development 11 min read

Comprehensive Domain Interface Design and Its Application in Backend Systems

This article explores the concept of fully interface‑based domain modeling, demonstrates how to design domain objects as interfaces, shows repository implementations for JPA, MyBatis and Elasticsearch, discusses association handling, and explains how such designs enable seamless switching between monolithic, clustered and microservice deployments.

Architecture Digest
Architecture Digest
Architecture Digest
Comprehensive Domain Interface Design and Its Application in Backend Systems

Designing service and repository objects as interfaces is common, but a deeper understanding of full interface‑based design—especially for domain models—is essential for flexible architecture.

Domain Interface Design

Instead of defining domain models only as classes, consider defining them as interfaces. For example:

public interface User {
    // ...
}

public class UserImpl implements User {
    // ...
}

Although this may seem unnecessary at first, it becomes valuable when adapting to different data sources.

When switching between ORM frameworks such as JPA and MyBatis, defining the domain object as an interface allows seamless replacement of the underlying implementation. A typical solution is to wrap persistence objects (PO) and domain objects (DO) and convert between them:

public class JpaUserRepository implements UserRepository {
    // ...
    @Override
    public Optional
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();
    }
}

By designing User as an interface, the conversion logic simplifies, allowing implementations such as:

public class JpaUserRepository implements UserRepository {
    // ...
    @Override
    public User create(String id) {
        return new JpaUser(id);
    }

    @Override
    public Optional
findById(String id) {
        JpaUser user = this.entityManager.find(JpaUser.class, id);
        return Optional.ofNullable(user);
    }

    @Override
    public User save(User user) {
        JpaUser target = JpaUser.of(user);
        return this.entityManager.merge(target);
    }
    // ...
}

The helper method JpaUser.of() copies properties from any User implementation to a concrete JpaUser 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;
    }
}

For repositories that rely on generic types (e.g., Spring Data Elasticsearch), the interface‑based domain model requires delegation because the generic parameter must be a concrete class:

public interface ElasticsearchUserRepository extends ElasticsearchRepository
{
    // extends ElasticsearchRepository
// Not supported
}

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
findById(String id) {
        return CastUtils.cast(this.elasticsearchUserRepository.findById(id));
    }

    @Override
    public User save(User user) {
        return this.elasticsearchUserRepository.save(ElasticsearchUser.of(user));
    }
    // ...
}

Association Interface Handling

Persistable entities cannot directly use interface‑typed fields because the persistence framework cannot determine the concrete class. For example:

@Entity
public class JpaOrder implements Order {
    // ...
    private List
items = new ArrayList<>(); // OrderItem is an interface, not persistable
}

To resolve this, JPA’s targetEntity attribute can specify the concrete class:

public class JpaOrder implements Order {
    // ...
    @OneToMany(targetEntity = JpaOrderItem.class)
    private List
items = new ArrayList<>();
}

Supported annotations include @OneToMany , @OneToOne , @ManyToOne , and @ManyToMany . When the framework does not support targetEntity , encapsulation can be used, as shown with an Elasticsearch implementation that converts generic OrderItem lists to concrete ElasticsearchOrderItem instances.

@Document(indexName = "user")
public class ElasticsearchOrder implements Order {
    private List
items = new ArrayList<>();

    @Override
    public void setItems(List
items) {
        this.items = Objects.requireNonNullElseGet(items, ArrayList::new)
                .stream().map(ElasticsearchOrderItem::of).collect(Collectors.toList());
    }
    // ...
}

For MyBatis, the mapping can be defined in OrderMapper.xml using <collection> with the concrete type.

<resultMap id="Order" type="org.mallfoundry.order.repository.mybatis.MybatisOrder">
    ...
    <collection property="items" ofType="org.mallfoundry.order.repository.mybatis.MybatisOrderItem">
        ...
    </collection>
    ...
</resultMap>

System‑Level Interface Design

By exposing domain objects, services, and other components as interfaces in a dedicated API module, different deployment scenarios (standalone, clustered, microservice) can switch implementations without changing client code. For example, a client depends on user in a monolithic setup, on user-openfeign-client in a microservice, or on user-rest-client in an external environment.

The open‑source Mallfoundry project demonstrates this approach: it is a Spring Boot‑based multi‑tenant e‑commerce platform that adopts DDD, interface‑based design, and modular architecture.

Project address: https://gitee.com/mallfoundry/mall

Conclusion

Domain object interface‑ification creates a unified contract across the system, enabling seamless adaptation to different data sources and deployment models, though it raises the expertise requirements for architects and developers.

JavaBackend DevelopmentDomain-Driven DesignSpringRepository PatternInterface Design
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

login 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.