Avoid Database Hijacking of the Domain: Proper Use of Repositories and Factories
The article explains why repository interfaces belong in the domain layer, implementations reside in the infrastructure layer, and factories should handle complex aggregate creation, illustrating how these patterns keep domain logic pure, enable dependency inversion, and simplify testing.
Why Repositories and Factories Matter
After a domain model is built, the application must create, save, and retrieve aggregates without letting database details pollute the domain layer. This chapter shows why the repository interface is defined in the domain layer, its implementation lives in the infrastructure layer, and factories are responsible for constructing complex, valid aggregates.
What Is a Repository?
Repository = a "pretended in‑memory collection" that lets you store and fetch aggregates like a List , while the underlying storage (MySQL, MongoDB, cache, etc.) remains invisible to the domain.
Calling orderRepository.save(order) stores the order; calling orderRepository.findById(id) returns it. The domain layer only cares about "store" and "retrieve", not how the data is persisted.
Problems with Direct Mapper Calls
Domain layer depends on persistence technology. Business logic mixes MyBatis/JPA concepts; changing ORM, adding cache, or sharding requires code changes.
Domain objects get ORM‑bound. To persist, Order needs a no‑arg constructor, setters, and annotations, breaking the aggregate’s invariants.
Testing becomes hard. Business logic tests must hit a database.
Applying dependency inversion solves these issues: the domain defines an interface for "what it needs to store and fetch", while the concrete implementation (JPA, MyBatis, etc.) lives in the infrastructure.
Step 1: Define the Repository Interface in the Domain Layer
// 位置:com.youxuan.store.order.domain
// 这是领域层的一部分,只有业务语义,没有任何数据库的影子
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByMemberId(MemberId memberId);
OrderId nextIdentity(); // 由仓储负责生成 ID
}The OrderRepository lives in the domain layer and describes "what storage capability the business needs" without mentioning SQL.
Step 2: Implement It in the Infrastructure Layer
// 位置:com.youxuan.store.order.infrastructure
// 这里才出现具体技术(JPA),领域层看不到它
@Repository
public class JpaOrderRepository implements OrderRepository {
private final OrderJpaDao jpaDao; // Spring Data JPA
private final OrderDataConverter converter; // 领域对象 <-> 数据对象 转换
@Override
public void save(Order order) {
OrderDO data = converter.toData(order); // 领域模型 → 数据库模型
jpaDao.save(data);
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaDao.findById(id.value())
.map(converter::toDomain); // 数据库模型 → 领域模型
}
@Override
public List<Order> findByMemberId(MemberId memberId) {
return jpaDao.findByMemberId(memberId.value())
.stream().map(converter::toDomain).collect(toList());
}
@Override
public OrderId nextIdentity() {
return new OrderId(idGenerator.next());
}
}The implementation contains all technical details; the domain never sees them.
Step 3: Inject the Repository into Application Services
@Service
public class PlaceOrderAppService {
private final OrderRepository orderRepo; // 依赖接口,不依赖实现
public PlaceOrderAppService(OrderRepository orderRepo) {
this.orderRepo = orderRepo; // Spring 自动注入 JpaOrderRepository
}
}This demonstrates the dependency‑inverted flow: high‑level domain code depends on an abstraction, low‑level infrastructure provides the concrete class.
Key Design Principles
One repository per aggregate root. Only the aggregate root (e.g., Order) gets a repository; internal entities like OrderItem are persisted together with the root.
Domain model ≠ database model. Use a converter/mapper to translate between rich domain objects and flat persistence tables. This keeps the domain pure while allowing storage‑optimal schemas.
Repositories handle only aggregate persistence. Complex queries belong to a separate read model (CQRS), not to the repository.
Factories for Complex Creation
When an aggregate’s creation involves many rules, assembling internal objects, and generating IDs, a factory should encapsulate that logic.
Static factory method on the aggregate root
Order order = Order.place(memberId, lines, address); // 工厂方法Independent factory class for more complex scenarios
public class OrderFactory {
private final ProductRepository productRepo;
private final PromotionCalculator promotionCalculator;
// 从购物车创建订单:要查商品快照、算促销,逻辑复杂,适合独立工厂
public Order createFromCart(Cart cart, Coupon coupon, Address address) {
List<OrderLine> lines = new ArrayList<>();
for (CartItem item : cart.getItems()) {
Product p = productRepo.findById(item.getProductId())
.orElseThrow(() -> new IllegalStateException("商品不存在"));
p.ensureSellable(); // 校验可售(业务规则在 Product 聚合里)
// 快照:把当前商品名和价格冻结进订单项
lines.add(new OrderLine(p.getId(), p.getName(), p.currentPrice(), item.getQty()));
}
Order order = Order.place(cart.getMemberId(), lines, address);
// 应用优惠券
Money discount = promotionCalculator.calculateDiscount(order, coupon);
order.applyDiscount(discount);
return order;
}
}The division of labor is clear: factories "create" new aggregates, repositories "store" and "retrieve" them.
Clarifying Common Doubts
Although OrderFactory depends on ProductRepository , this does not violate the rule that entities should not inject repositories. Coordinators (factories, domain services) may depend on repository interfaces; the aggregate root itself must remain free of such dependencies.
Comparing Traditional Mapper Approach vs. DDD Repository + Factory
Domain layer dependency: Traditional code depends on MyBatis/JPA; DDD approach depends only on its own interfaces.
Changing ORM or adding cache: Traditional code requires business‑logic changes; DDD changes only the infrastructure implementation.
Encapsulation: Traditional code forces setters on entities, breaking encapsulation; DDD keeps entities pure.
Unit testing: Traditional code needs a database; DDD interfaces can be mocked for pure in‑memory tests.
Complex object creation: Traditional logic spreads across services; DDD factories guarantee "born valid" aggregates.
How to Verify the Repository Does Not Pollute the Domain
1. Domain layer should not import infrastructure classes
# 领域层不应该 import Spring Data、MyBatis、Redis、MQ 等技术类
grep -R "org.springframework.data\|org.apache.ibatis\|RedisTemplate\|KafkaTemplate\|RocketMQ" src/main/java/com/youxuan/store/*/domainIf results appear, check whether repository implementations or mappers have been mistakenly placed in the domain.
2. Repository interfaces should serve only aggregate roots
# 如果出现 OrderItemRepository、AddressRepository 这类内部对象仓储,要重点审查
find src/main/java -name "*Repository.java" | sortIdeally you see OrderRepository but not OrderItemRepository; internal entities are persisted together with the root.
3. Assign responsibilities correctly
Creating a valid order from a cart, product snapshot, and coupon → Factory
Saving an order or fetching by ID → Repository
Complex admin‑page queries → Query DAO / CQRS
Changing order status → Aggregate root (no repository)
With these checks, the tactical design is complete. The next chapter will show how to place these building blocks within a Spring Boot project’s layered architecture.
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.
Yumin Fish Harvest
A deep‑sea salvage fisherman sharing architecture insights, practical tips, and lessons learned.
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.
