DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Java Developers: Build Something Awesome with Copilot CLI and Win Big Prizes!
  • Build an AI Chatroom With ChatGPT and ZK by Asking It How!
  • How To Build a Google Photos Clone - Part 1
  • How To Build a Command-Line Text Editor With Java (Part 3)

Trending

  • Architecting Petabyte-Scale Hyperspectral Pipelines on AWS
  • Stop Writing Dialect-Specific SQL: A Unified Query Builder for Node.js
  • Evaluating SOC Effectiveness Using Detection Coverage and Response Metrics
  • Building a Skill-Based Agentic Reviewer with Claude Code: A Practical Guide Using Skills.MD, MCP Servers, Tools, and Tasks
  1. DZone
  2. Coding
  3. Java
  4. Introduction to Tactical DDD With Java: Steps to Build Semantic Code

Introduction to Tactical DDD With Java: Steps to Build Semantic Code

Learn about the core Tactical DDD patterns in Java in order to build semantic, maintainable, and business-oriented code.

By 
Otavio Santana user avatar
Otavio Santana
DZone Core CORE ·
May. 20, 26 · Analysis
Likes (3)
Comment
Save
Tweet
Share
1.5K Views

Join the DZone community and get the full member experience.

Join For Free

Modern software systems rarely fail due to poor coding skills. Most failures occur when teams lose sight of the business problem they are addressing. As systems evolve, requirements shift, teams expand, and new integrations are added, codebases often become collections of technical decisions that lack business context. Classes become generic managers and services, methods devolve into procedural scripts, and communication between developers and domain experts diminishes. Tactical Domain-Driven Design (DDD) addresses this issue by emphasizing software that directly reflects business language in code, rather than focusing solely on infrastructure or frameworks.

The term “semantic” comes from the Greek semantikos, meaning “significant” or “meaningful,” which is central to Tactical DDD. The objective is not just to reorganize classes, but to ensure code communicates intent clearly to both engineers and business experts. In modern Java systems, where complexity increases due to distributed architectures, integrations, and ongoing business changes, this clarity is essential for long-term maintainability. 

Tactical DDD provides practical patterns, such as entities, value objects, aggregates, repositories, factories, and domain services, to preserve codebase meaning and manage complexity. This article will examine these patterns step by step using Java and a soccer championship scenario to show how semantic code improves system understanding, evolution, and maintenance.

Entity

Before applying Tactical DDD patterns, it is important to recognize that they should not be the starting point of the design process. A common mistake in software projects is to begin with entities, repositories, and aggregates without first understanding the business. Tactical patterns serve as implementation tools, not discovery tools. Strategic DDD should begin with defining domain boundaries, the ubiquitous language, and the business context. Only after clarifying the problem space should you translate that understanding into code using tactical patterns.

An Entity is a core Tactical DDD pattern. The term originates from the Latin ens, meaning “being” or “existing thing.” In software design, it refers to maintaining its identity throughout its lifecycle. An entity is defined not by its current attributes, but by the business’s recognition of it as the same conceptual object over time. Entities are useful when the domain must track the lifecycle of something important to the business.

In a soccer championship, a player is a clear example of an entity. A player may change teams, positions, salary, or statistics during a career, but the system continues to recognize the player as the same individual within the domain. Therefore, identity is more important than changes to attributes. The following Java class illustrates this concept:

Java
 
import java.util.UUID;

public class Player {

    private UUID id;

    private String name;

    private Position position;

    public Player(UUID id, String name, Position position) {
        this.id = id;
        this.name = name;
        this.position = position;
    }
}


The Player class demonstrates the Entity pattern by using a unique identifier in the id field. This identifier enables the application to distinguish one player from another, regardless of changes to other attributes. While name and position may change, the identity remains constant. This characteristic defines the object as an entity rather than a simple data structure or value object.

Value Object

Entities are defined by identity, but not all domain concepts require lifecycle tracking or unique identification. Many business concepts describe characteristics, measurements, classifications, or immutable meanings. The Value Object pattern addresses these cases. Here, “value” means the object is defined solely by its attributes, not by identity. In Tactical DDD, value objects reduce the need for primitives and clarify the domain language within the codebase.

A Value Object is an immutable object that represents a descriptive aspect of the domain. Unlike entities, two value objects with identical values are considered the same. Value objects are often used for concepts such as money, addresses, coordinates, statuses, measurements, or classifications. Their primary purpose is to improve semantic clarity and encapsulate domain rules for specific concepts.

In a soccer championship scenario, player position is a good example of a value object because the application does not need to track its lifecycle. The domain is concerned only with the meaning of the value. The following Java enum illustrates this concept:

Java
 
public enum Position {
    GOALKEEPER,
    DEFENDER,
    MIDFIELDER,
    FORWARD
}


The Position enum applies the Value Object pattern by representing a business classification rather than using primitive strings throughout the codebase. By using explicit types instead of raw text, such as "forward" or "goalkeeper", the application improves readability, reduces invalid states, and reinforces a shared language between developers and domain experts.

Factory

As domain models evolve, object creation often requires more than calling a constructor. Business rules, validations, default values, and initialization steps may spread into controllers, services, or application layers, leading to duplication and fragmented domain knowledge. The Factory pattern centralizes object creation, ensuring new domain objects are valid and meaningful.

A Factory is a Tactical DDD pattern that encapsulates the creation logic of entities or aggregates. Its purpose is not just to “hide the constructor,” but to express domain intent during object creation. Originating from manufacturing, factories ensure objects are assembled correctly before use. In software, this approach maintains consistency and enforces business rules during instantiation.

In a soccer championship scenario, creating a player requires more than allocating memory. A new forward player must have the correct position and a unique identity. Centralizing this logic in a factory prevents duplication across the system.

Java
 
import java.util.UUID;

public class PlayerFactory {

    public Player createFoward(String name) {
        return new Player(
                UUID.randomUUID(),
                name,
                Position.FORWARD
        );
    }
}


PlayerFactory implements the Factory pattern by encapsulating the details of Player creation. The application layer does not need to manage identifier generation or position assignment for a forward player. The method name communicates business intent, allowing the code to express a meaningful domain operation rather than low-level construction details.

Aggregate Root

As systems scale, maintaining consistency between related entities becomes more challenging. Without clear boundaries, business rules can spread across services, repositories, and transactions. Tactical DDD addresses this by defining explicit consistency boundaries within the domain model. Aggregate and Aggregate Root patterns are essential to this approach.

An Aggregate is a group of related entities and value objects managed as a single consistency boundary. The Aggregate Root serves as the main entry point, coordinating and protecting the aggregate’s internal state. In practice, the aggregate root ensures controlled modifications and maintains business rule consistency during state changes.

In a soccer championship scenario, a team acts as an aggregate root by managing the lifecycle and consistency of its players. The application should not modify the player collection directly; all changes should occur through the team’s defined behaviors:

Java
 
import java.util.Collections;
import java.util.List;

public class Team {

    private TeamId id;

    private String name;

    private List<Player> players;

    public Team(TeamId id, String name, List<Player> players) {
        this.id = id;
        this.name = name;
        this.players = players;
    }

    public List<Player> getPlayers() {
        return Collections.unmodifiableList(players);
    }

    public void remove(Player player) {
        players.remove(player);
    }

    public void add(Player player) {
        players.add(player);
    }
}


The Team class implements the Aggregate Root pattern, managing access to its player collection. Rather than allowing direct modification, it has add and remove. This method enables business rules to evolve and ensures consistency across the application. The aggregate root safeguards the domain boundary and maintains the cycle.

Repository

One of the biggest challenges in enterprise applications is avoiding tight coupling between business logic and persistence concerns. Over time, SQL queries, database operations, caching logic, and infrastructure details can start leaking into the domain layer, making the code harder to maintain and evolve. Tactical DDD addresses this problem with the Repository pattern, which provides a collection-like abstraction for managing aggregates.

The term “repository” originates from the Latin repositorium, meaning “a place where things are stored.” In Domain-Driven Design, a repository is not merely a DAO or a utility class for executing queries. Its primary goal is to provide access to aggregates while hiding infrastructure complexity from the domain model. A repository allows the application to work with domain concepts instead of persistence mechanisms, preserving the separation between business logic and technical implementation.

In the soccer championship scenario, the application needs a mechanism to persist and retrieve teams without exposing database details to the business flow. Since Team acts as the aggregate root, the repository is responsible for managing it as a consistency boundary:

Java
 
public interface TeamRepository {

    Team save(Team team);
}


The TeamRepository applies the Repository pattern by abstracting persistence operations behind a domain-oriented contract. The application layer does not need to know whether the data is stored in PostgreSQL, MongoDB, Redis, or another technology. More importantly, the repository communicates the business intention directly through the aggregate itself. Instead of manipulating tables or records, the code works with meaningful domain concepts such as Team, preserving the semantic clarity of the model and reducing coupling between the domain and infrastructure layers.

Domain Service

Not every business operation fits within an entity or aggregate root. As the domain evolves, some rules involve multiple entities or coordination logic that do not belong to a single object. Assigning these responsibilities to entities can lead to bloated models and reduced cohesion. Tactical DDD addresses this with the Domain Service pattern.

A Domain Service contains business logic that does not belong to a specific entity or value object, but remains part of the domain model. Its role is to execute meaningful business operations involving multiple domain objects, not to handle technical orchestration or infrastructure. In DDD, a service encapsulates domain behavior across aggregates while maintaining the model’s clarity.

In the soccer championship scenario, transferring a player between teams is a business operation involving multiple aggregates. The responsibility does not belong exclusively to the player or to a single team. Instead, the operation represents a domain action coordinating both source and destination teams:

Java
 
public class TransferService {

    public void transfer(
            Player player,
            Team source,
            Team destination) {

        source.remove(player);
        destination.add(player);
    }
}


TransferService implements the Domain Service pattern by encapsulating the business logic for transferring a player between teams. This service expresses the domain concept directly, rather than spreading logic across controllers or application layers. The method communicates business intent clearly using the domain’s ubiquitous language. Instead of exposing low-level details, the code now reflects a meaningful operation recognized by both developers and business experts: transferring a player during the championship lifecycle.

Domain Event

In complex systems, important business actions rarely affect only a single part of the application. A change in one domain often triggers reactions in other contexts, such as notifications, analytics, integrations, auditing, or external workflows. Directly coupling these concerns creates rigid architectures in which every new requirement increases dependencies across the system. Tactical DDD addresses this challenge with the Domain Event pattern.

A Domain Event represents an important event that has already occurred within the business domain. The emphasis on the past tense is intentional because events describe facts, not commands or intentions. The term “event” originates from the Latin eventus, meaning “outcome” or “occurrence.” In Domain-Driven Design, domain events allow systems to communicate meaningful business changes while reducing coupling between components and bounded contexts. Instead of directly invoking every dependent operation, the domain publishes events that other parts of the system may react to independently.

In the soccer championship scenario, hiring a new player is an important business occurrence that other parts of the system may care about. The championship may want to notify fans, update statistics, trigger merchandising actions, or synchronize with external systems. Instead of embedding all these responsibilities directly into the transfer logic, the application can represent the occurrence explicitly through a domain event:

Java
 
public record NewSoccerHired(
        Team team,
        Player player) {
}


The event can then be published once the business operation finishes successfully:

Java
 
eventPublisher.publish(
        new NewSoccerHired(destination, player)
);


The NewSoccerHired record applies the Domain Event pattern by representing a meaningful business fact inside the domain model. Instead of tightly coupling multiple responsibilities, the system now exposes a semantic business occurrence that other parts of the architecture can react to independently. This approach improves extensibility, reduces direct dependencies, and preserves the ubiquitous language across the application lifecycle.

Application Service

As systems evolve, business operations often require coordination across domain components, persistence, and integration points. Without a clear orchestration layer, this logic may spread across controllers, APIs, and infrastructure classes, resulting in tightly coupled, hard-to-maintain applications. Tactical DDD addresses this with the Application Service pattern.

An Application Service orchestrates use cases and coordinates domain operations. Unlike domain services, which encapsulate business rules, an application service manages the execution flow of business actions. In DDD, it serves as the coordination layer, connecting repositories, domain operations, and external interactions, while keeping the domain model focused on business behavior.

In a soccer championship scenario, transferring a player between teams requires more than one business rule. This operation coordinates transfer logic, persistence, and event publication. The following class centralizes this orchestration in a single use case:

Java
 
public class TransferPlayerUserCase {

    private final TeamRepository teamRepository;
    private final TransferService transferService;
    private final EventPublisher eventPublisher;

    public TransferPlayerUserCase(
            TeamRepository teamRepository,
            TransferService transferService,
            EventPublisher eventPublisher) {
        this.teamRepository = teamRepository;
        this.transferService = transferService;
        this.eventPublisher = eventPublisher;
    }

    public void execute(
            Player player,
            Team source,
            Team destination) {

        transferService.transfer(player, source, destination);

        teamRepository.save(source);
        teamRepository.save(destination);

        eventPublisher.publish(
                new NewSoccerHired(destination, player)
        );
    }
}


The TransferPlayerUserCase demonstrates the Application Service pattern by orchestrating the entire player transfer process. Rather than placing orchestration logic in controllers or entities, this class coordinates domain operations, persistence, and event publication within a single workflow. The method represents a meaningful business action within the domain: transferring a player between teams during the championship.

Conclusion

Tactical Domain-Driven Design does not aim to add unnecessary complexity or apply patterns indiscriminately. Its purpose is to help engineers build software that clearly communicates business meaning through code. By introducing concepts such as entities, value objects, factories, aggregates, repositories, domain services, domain events, and application services, developers create systems that are easier to understand, maintain, and adapt as business needs evolve. Tactical DDD also bridges the gap between technical and business perspectives, making code a semantic representation of the domain.

This article introduces core Tactical DDD patterns using Java and a soccer championship scenario. The aim is not to cover every aspect of Domain-Driven Design, but to show how these patterns help build expressive and maintainable systems. As projects become more complex, preserving business meaning within the codebase is increasingly important, especially in modern distributed architectures where technical complexity can obscure domain language.


Build (game engine) Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Java Developers: Build Something Awesome with Copilot CLI and Win Big Prizes!
  • Build an AI Chatroom With ChatGPT and ZK by Asking It How!
  • How To Build a Google Photos Clone - Part 1
  • How To Build a Command-Line Text Editor With Java (Part 3)

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook