Skip to content
Leo Jung
Go back

[DDD Intro #17] Factory: Creation also contains domain knowledge

Edit page

Object creation is often treated as a simple technical problem. It is easy to think that we only need to call a constructor, pass the needed values, and fill fields. But in a domain model, creation can also be an important behavior.

Some objects must not be created with arbitrary values. They must be valid from the moment they are created, satisfy domain rules, and have necessary components created together in a consistent state.

Factory is a pattern for handling this creation responsibility. Factory is not simply a tool for hiding new. It is a way to explicitly express the responsibility of creating valid domain objects.

When a constructor is enough

Not every object needs a Factory. For a simple Value Object, a constructor or static factory method may be enough.

Money fee = new Money(9900, Currency.KRW);
EmailAddress email = EmailAddress.of("user@example.com");

This level of creation has no complex procedure. The object itself only needs to validate itself.

Even for Entity, a static factory method can be enough when the case is simple.

Subscription subscription = Subscription.startTrial(memberId, trialPeriod, now);

If this method sufficiently expresses the rules needed to create a subscription and does not need much external collaboration, there is no need to create a separate Factory.

Factory should be considered when creation logic becomes complex or when putting creation responsibility inside the Entity feels burdensome.

Creation also has domain rules

Think about a subscription service. Starting a new subscription can have several rules.

A free trial is allowed only once per account.
An enterprise plan must satisfy the minimum seat count.
An annual subscription has a discount policy applied.
A promotion code can be used only for certain plans.
A paid subscription cannot be activated before payment is completed.

If these rules are assembled as values inside an Application Service, the creation process scatters.

Subscription subscription = new Subscription();
subscription.setMemberId(memberId);
subscription.setPlanId(planId);
subscription.setStatus(ACTIVE);
subscription.setPeriod(period);
subscription.setPrice(price);

This code is closer to assembly than creation. It also makes it easy to create an invalid subscription.

If an object is important in the domain, it should be valid from the moment it is created.

Subscription subscription = subscriptionFactory.createPaidSubscription(
    member,
    plan,
    paymentResult,
    promotionCode,
    now
);

This code reveals the domain behavior of “creating a paid subscription.” Creation rules can also be gathered inside the Factory.

Static factory method inside Entity

If creation logic naturally belongs to the object itself, a static factory method is a good choice.

public class Subscription {
    public static Subscription startTrial(MemberId memberId, TrialPeriod trialPeriod, Instant now) {
        return new Subscription(
            SubscriptionId.newId(),
            memberId,
            PlanId.trial(),
            SubscriptionStatus.TRIAL,
            trialPeriod.toSubscriptionPeriod(now)
        );
    }
}

The advantage of this approach is that the creation intent is clear.

Subscription trial = Subscription.startTrial(memberId, trialPeriod, now);

This is much easier to read than a constructor that receives many parameters.

new Subscription(id, memberId, planId, TRIAL, startDate, endDate);

Static factory methods fit well when creation scenarios are clear and few, and when external policies or Repository lookups are not needed.

When a separate Factory is needed

A separate Factory is useful when creation logic is too complex to place inside one object, or when several domain objects and policies are needed.

For example, suppose creating a paid subscription requires this information:

Member information
Plan information
Promotion policy
Payment result
Free trial history
Seat count policy

If all of this goes into a static method of Subscription, Subscription knows too much. In this case, we can create SubscriptionFactory or a Factory with a more specific name.

public class SubscriptionFactory {
    public Subscription createPaidSubscription(
        Member member,
        Plan plan,
        PaymentResult paymentResult,
        Promotion promotion,
        Instant now
    ) {
        if (!paymentResult.isSucceeded()) {
            throw new CannotStartSubscriptionException("Payment is not completed.");
        }

        Money price = promotion.applyTo(plan.price());
        SubscriptionPeriod period = plan.billingCycle().periodFrom(now);

        return Subscription.paid(member.id(), plan.id(), price, period, now);
    }
}

Here, the Factory has the responsibility of creating a valid subscription. This is not simple object assembly. It is creation that satisfies domain rules.

Boundary between Factory and Application Service

Application Service can also create objects. But when creation rules become complex, Application Service ends up making too many domain judgments.

if (!paymentResult.isSucceeded()) {
    throw new IllegalStateException();
}

if (plan.isEnterprise() && seatCount < plan.minimumSeatCount()) {
    throw new IllegalStateException();
}

Subscription subscription = new Subscription(...);

When this kind of code accumulates in Application Service, creation rules mix with use case flow.

It is clearer for Application Service to load the needed objects and ask a Factory to create the object.

Subscription subscription = subscriptionFactory.create(command, member, plan, paymentResult, now);
subscriptionRepository.save(subscription);

In this structure, Application Service coordinates the flow, and Factory handles creation rules.

Factory hides incomplete objects

When objects are assembled step by step, an incomplete state can exist for a while.

Subscription subscription = new Subscription();
subscription.setMemberId(memberId);
subscription.setPlanId(planId);
subscription.setStatus(ACTIVE);

The object in the middle is invalid. Required values may be missing, or invariants may not be satisfied. If this kind of object is exposed outside, bugs are likely.

Factory hides this assembly process internally and returns only a completed, valid object.

Subscription subscription = subscriptionFactory.createTrial(member, now);

The caller does not need to know the details of the creation process. It only needs to trust that the returned object is valid.

This is an important value of Factory. It encapsulates complex creation and lets only valid domain objects enter the world.

Names should reveal creation intent

Factory method names should be more specific than simply create. In the domain, the same object can be created in several ways.

createTrialSubscription(...)
createPaidSubscription(...)
createEnterpriseSubscription(...)
restoreFromLegacy(...)

Each method represents a different creation scenario. A free trial subscription and a paid subscription can both be Subscription, but their creation rules can differ.

When names are specific, creation rules are more visible. Conversely, if all creation is handled by one create method, conditionals increase and the Factory itself can become complicated.

Closing

Factory is not a mechanical pattern for hiding object creation. It is a way to clearly express the responsibility of creating valid objects in the domain.

If creation is simple, a constructor or static factory method is enough. But if several domain rules are intertwined in creation and several objects or policies must be considered together, a separate Factory helps.

What matters is that an object should be valid from the moment it is born. Creating an object in an invalid state and fixing it later makes the domain model unstable.

In DDD, creation is not simple assembly. Creation also contains domain knowledge. Factory is a tool that keeps that knowledge from being lost inside code.


Edit page
Share this post:

Previous Post
[DDD Intro #18] Specification and Policy: Modeling conditions and policies
Next Post
[DDD Intro #16] Domain Service: Domain logic that is hard to force into an object