Architecture Intermediate 12 min

Enforcing Invariants: Factory Methods and Private Constructors

Stop creating invalid objects. Learn how to use static factory methods and private constructors to ensure DDD entities are always in a valid state.

By Victor Robin Updated:

Introduction

Early in BlueRobin’s development, I discovered a recurring bug pattern: documents were being created with null owners and zero-byte file sizes. The root cause was always the same—public constructors that allowed callers to skip validation entirely. Switching to factory methods with private constructors eliminated this entire category of bugs overnight.

Allowing code to execute new Document() { Id = null } is a bug waiting to happen. In Domain-Driven Design (DDD), an entity should never exist in an invalid state. If you have an object, it must be valid. [Domain-Driven Design: Tackling Complexity in the Heart of Software] — Eric Evans , 2003

Why encapsulate creation?

  • Guarantees: If you hold a reference to an entity, you know its rules are satisfied.
  • Expressiveness: Document.ImportUserUpload(...) tells a story. new Document(...) does not.
  • Validation: You can throw exceptions before the object is created. [Implementing Domain-Driven Design] — Vaughn Vernon , 2013

What We’ll Build

  1. Private Constructor: Locking down the default instantiation.
  2. Static Factory Method: The only gateway to creating an instance.
  3. Invariant Checks: Validating input before it touches the state.

Architecture Overview

This pattern follows the tactical patterns laid out in DDD literature, where factories encapsulate the complexity of object creation. [Domain-Driven Design Guidance for .NET] — Microsoft , 2024

flowchart LR
    Client["👨‍💻 Client"] -->|Call| Factory["🏭 Static Create()"]
    
    subgraph Entity["📄 Document Entity"]
        Factory -->|Check| Rules{"❓ Invariants\nPassed?"}
        Rules -->|No| Exception["❌ Throw Error"]
        Rules -->|Yes| Ctor["🔒 Private Constructor"]
        Ctor --> Instance["✅ Valid Instance"]
    end

    classDef primary fill:#7c3aed,stroke:#fff,color:#fff
    classDef secondary fill:#06b6d4,stroke:#fff,color:#fff
    classDef db fill:#f43f5e,stroke:#fff,color:#fff
    classDef warning fill:#fbbf24,stroke:#fff,color:#fff

    class Client warning
    class Entity primary

Implementation

The Anti-Pattern

This is what we want to avoid. The “Property Bag” where anything goes. The principle of keeping all entities valid at all times is a core tenet of object-oriented discipline. [Object Calisthenics] — William Durand (after Jeff Bay) , 2013

// ❌ BAD: Anemic Model
public class Document
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    
    // Anyone can create an empty, invalid document
    public Document() { } 
}

// Result:
var doc = new Document(); // Has empty Guid, null Title.
_repo.Save(doc); // Database crash or corrupt data.

Section 2: The Factory Method

The factory method pattern provides a controlled entry point for object creation, ensuring that all invariants are satisfied before an instance is returned. [Factory Method] — Refactoring Guru , 2024

We use a private constructor for EF Core (if needed) and a public static factory method for the domain.

public class Document : AggregateRoot<CustomId>
{
    public string Title { get; private set; }
    public long SizeBytes { get; private set; }
    public DateTimeOffset CreatedAt { get; private set; }

    // 1. Private Constructor
    // Only accessible by this class (and EF Core via reflection)
    private Document(CustomId id, string title, long sizeBytes)
    {
        Id = id;
        Title = title;
        SizeBytes = sizeBytes;
        CreatedAt = DateTimeOffset.UtcNow;
    }

    // 2. Static Factory Method
    public static Document Create(CustomId id, string title, long sizeBytes)
    {
        // 3. Invariant Checks
        if (string.IsNullOrWhiteSpace(title))
            throw new DomainException("Title cannot be empty.");

        if (sizeBytes <= 0)
            throw new DomainException("Document cannot be empty.");

        // 4. Create and Return
        var doc = new Document(id, title.Trim(), sizeBytes);
        
        doc.AddDomainEvent(new DocumentCreatedEvent(id));
        
        return doc;
    }
}

Section 3: Usage

Now the client code is clean and safe.

try 
{
    var doc = Document.Create(
        CustomId.NewId(), 
        "Tax Returns 2025.pdf", 
        1024 * 500
    );
    // If we get here, 'doc' is GUARANTEED to be valid.
}
catch (DomainException ex)
{
    // Handle specific domain errors
}

Conclusion

By closing the front door (public constructors) and opening a guarded side door (factory methods), you eliminate an entire category of bugs related to invalid state. Your entities become trustworthy building blocks for your application logic.

Looking back, the shift to factory methods was one of the highest-impact changes I made in BlueRobin’s domain model. The initial cost was refactoring about 40 call sites from new Document(...) to Document.Create(...), but the payoff was immediate: the number of null-reference exceptions in production dropped to zero. Every time a new developer (or future me) interacts with the domain, they are guided toward the correct usage by the API itself.

Next Steps:

  • Learn how Value Objects bring the same discipline to primitive types.
  • See how Aggregates compose entities with factory methods into consistent boundaries.

Further Reading