Java

Domain Modeling in Java: Building Robust and Scalable Structures (Part 1 - Unidirectional)

Domain modeling is a key concept in software design, especially object-oriented programming and domain-driven design (DDD). In this post, we'll look at how to design appropriate domains, map relationships between them, then implement those interactions in pure Java, utilizing code samples and UML diagrams.

Understanding Domain Modeling Concepts

  1. What is a Domain Model?

Domain modeling is the process of developing a conceptual model that depicts real-world objects and their interactions. In Java, these things are frequently represented as classes, which serve as the foundation for developing solid and scalable applications.
For example, in an e-commerce application, you might have entities such as Customer, Order, and Product. Understanding and defining the relationships between these elements is critical for creating a well-organized application.

  1. Key Concepts in Domain Modeling

Entities/Domain

Entities are objects with distinct identities. They are typically mutable and can evolve over time. Entities in Java are often represented as classes with a unique identifier.

public class User {
    private Long id;
    private String name;
    private String email;
    // Constructors, getters, and setters
}

Why is Domain Modeling Important?

Domain modeling is an important part of software development since it directly affects the quality, maintainability, and scalability of your application. Here are the primary reasons why domain modeling is important:

  • Clarity: Provides a clear understanding of the business logic.
  • Reusability: Encourages reusable and modular code.
  • Scalability: Lays a solid foundation for future application growth.

Associations in Domain Modeling

An association denotes the relationship between two or more entities. It describes how instances relating to one entity are related to instances of another.

Properties of an Association

  1. Name: The name of an association should describe the relationship, often using a verb (e.g., "manages", "owns").
  2. Multiplicity (Cardinality): Specifies how many instances of an entity can be associated with another entity.
  • 1 (One): Exactly one instance.
  • 0..1 (Optional): Zero or one instance.
  • *..1(One or Many): One or more instances.
  • *..0 (Zero or Many): Zero or more instances.
  1. Attributes: Associations can have attributes that further define the relationship.

Types of Associations

1. Unidirectional Association

In a unidirectional association, one class knows of the other, but the reverse is not true.

public class User { 
private Profile profile;
 }
public class Profile{ }

Unidirectional with Multiplicities

A unidirectional association may be 1-0..1, 1-1, or one-many

General Rules

One-one Multiplicity:

unidirectional model

  1. Association: Each instance of Entity A is associated with exactly one instance of Entity B. Each instance of Entity B is associated with exactly one instance of Entity A.

Ownership:

One entity typically "owns" the relationship. For example, Entity A may own Entity B, meaning Entity B cannot exist without Entity A.

  1. Maintaining the Relationship: When an instance of Entity A is created, it must be equipped with an instance of Entity B. It is not possible to add a second instance of Entity B to an instance of Entity A. It is not possible to create an instance of Entity B on its own; it must be created as a property of Entity A.

The same rules apply to the User and Profile example:

  1. Association: Each User has exactly one Profile. Each Profile belongs to exactly one User.
  2. Navigability: The relationship is unidirectional: You can navigate from User to Profile and not from Profile to User. It means Profile does not maintain a reference of User.
  3. Ownership: The User maintains a reference to Profile. A Profile cannot exist without a User.
  4. Maintaining the Relationship: When a User is created, it is equipped with a Profile. It is not possible to add a second Profile to a User. It is not possible to create a Profile on its own; it must be created as a property of a User.
public class User {
    private Long id;
    private String name;
    private Profile profile; // One-to-One relationship with Profile

    // Constructor
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
        this.profile = new Profile(); // Equip the User with a Profile
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Profile getProfile() {
        return profile;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", profile=" + profile +
                '}';
    }
}
public class Profile {
    private Long id;
    private String bio;

    // Constructor
    public Profile() {
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getBio() {
        return bio;
    }

    public void setBio(String bio) {
        this.bio = bio;
    }

    @Override
    public String toString() {
        return "Profile{" +
                "id=" + id +
                ", bio='" + bio + '\'' +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        User user = new User(1L,"Leo");
        user.getProfile().setId(1L);
        user.getProfile().setBio("User Bio");

        System.out.println(user.toString());
//output: User{id=1, name='Leo', profile=Profile{id=1, bio='User Bio'}}
    }
}

One to zero..oneMultiplicity:

In a unidirectional one-to-zero-or-one relationship, the relationship is navigable in only one direction. For example:

  • You can navigate from User to Profile, but not from Profile to User.
  • Each User may have zero or one Profile.
  • Each Profile belongs to exactly one User.

1. User Entity

The User class represents a user in the system. Each user has a unique ID, a name, and an optional reference to their Profile.

public class User {
    private Long id;
    private String name;
    private Profile profile; // maintain reference to profile

    // Constructor
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Profile getProfile() {
        return profile;
    }

    public void setProfile(Profile profile) {
        this.profile = profile;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", profile=" + profile +
                '}';
    }
}

2. Profile Entity

The Profile class represents the profile of a user. Each profile has a unique ID and a bio. Notice that there is no reference to the User in this class, making the relationship unidirectional.

public class Profile {
    private Long id;
    private String bio;


    //make constructor private and use factory method to create profile instance
    private Profile(Long id, String bio,User user) {
        this.id = id;
        this.bio = bio;
        user.setProfile(this);
    }
    //Factory method
    public static  Profile newProfile(Long id, String bio,User user) {
        if(user == null) {
            throw new NullPointerException("user is null");
        }
        return new Profile(id, bio, user);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getBio() {
        return bio;
    }

    public void setBio(String bio) {
        this.bio = bio;
    }

    @Override
    public String toString() {
        return "Profile{" +
                "id=" + id +
                ", bio='" + bio + '\'' +
                '}';
    }
}

3. Establishing the Relationship

The relationship is established when the Profile is assigned to the User. The Profile cannot exist without a User, but the User can exist without a Profile.

public class Main {
    public static void main(String[] args) {
        User user = new User(1L,"Bob Lee");
        Profile profile = Profile.newProfile(1L,"Bob Lee Profile",user);
        System.out.println(user);
    }
}

One-many Multiplicity

General Rules for Unidirectional One-to-Many Multiplicity

  1. Association:
  2. Each instance of Entity A is associated with zero or more instances of Entity B.
  3. Each instance of Entity B is associated with exactly one instance of Entity A.
  4. Navigability:
  5. The relationship is unidirectional: You can navigate from Entity A to Entity B, but not from Entity B to Entity A.
  6. Ownership:
  7. Entity A "owns" the relationship. Entity B cannot exist without Entity A, but Entity A can exist without Entity B.
  8. Maintaining the Relationship:
  9. When an instance of Entity A is created, it is equipped with a (possibly empty) collection of Entity B.
  10. It is not possible to create an instance of Entity B on its own; it must be created as part of the collection of Entity A.

One to zero or Many

1. User Entity

The User class represents a user in the system. Each user has a unique ID, a name, and a list of Address objects.

public class User {
    private Long id;
    private String name;
    private List<Address> addresses; // Unidirectional One-to-Zero-or-Many relationship with Address

    // Constructor
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
        this.addresses = new ArrayList<>();
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Address> getAddresses() {
        return addresses;
    }

    // Method to add an Address
    public void addAddress(Address address) {
        if (address == null) {
            throw new IllegalArgumentException("Address cannot be null.");
        }
        this.addresses.add(address);
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", addresses=" + addresses +
                '}';
    }
}

2. Address Entity

The Address class represents the address of a user. Each address has a unique ID, a street, a city, and a zip code. Notice that there is no reference to the User in this class, making the relationship unidirectional.

public class Address {
    private Long id;
    private String street;
    private String city;
    private String zipCode;

    // Constructor
    public Address(Long id, String street, String city, String zipCode) {
        this.id = id;
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }

    public String getZipCode() {
        return zipCode;
    }

    @Override
    public String toString() {
        return "Address{" +
                "id=" + id +
                ", street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}

3. Establishing the Relationship

The relationship is established when an Address is added to the User. The Address cannot exist without a User, but the User can exist without an Address.

public class Main {
    public static void main(String[] args) {
        // Create a User without any Addresses
        User user1 = new User(1L, "Alice");
        System.out.println(user1);
        // Create a User with multiple Addresses
        User user2 = new User(2L, "Bob");
        Address address1 = new Address(1L, "123 Main St", "Springfield", "12345");
        Address address2 = new Address(2L, "456 Elm St", "Shelbyville", "67890");
        user2.addAddress(address1);
        user2.addAddress(address2);
        System.out.println(user2);
    }
}

output:

User{id=1, name='Alice', addresses=[]}
User{id=2, name='Bob', addresses=[Address{id=1, street='123 Main St', city='Springfield', zipCode='12345'}, Address{id=2, street='456 Elm St', city='Shelbyville', zipCode='67890'}]}
Error: Address cannot be null.

General Rules for One-to-One-or-Many Multiplicity

  1. Association:
  2. Each instance of Entity A is associated with one or more instances of Entity B.
  3. Each instance of Entity B is associated with exactly one instance of Entity A.
  4. Navigability:
  5. The relationship is unidirectional: You can navigate from Entity A to Entity B, but not from Entity B to Entity A.
  6. Ownership:
  7. Entity A "owns" the relationship. Entity B cannot exist without Entity A.
  8. Maintaining the Relationship:
  9. When an instance of Entity A is created, it must be equipped with at least one instance of Entity B.
  10. It is not possible to create an instance of Entity B on its own; it must be created as part of the collection of Entity A.

public class User {
    private Long id;
    private String name;
    private List<Address> addresses = new ArrayList<>(); // Unidirectional One-to-One-or-Many relationship with Address

    // Constructor
    public User(Long id, String name, Address initialAddress) {
        this.id = id;
        this.name = name;
        this.addAddress(initialAddress);
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Address> getAddresses() {
        return addresses;
    }

    // Method to add an Address
    public void addAddress(Address address) {
        if (address == null) {
            throw new IllegalArgumentException("Address cannot be null.");
        }
        this.addresses.add(address);
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", addresses=" + addresses +
                '}';
    }
}
public class Address {
    private Long id;
    private String street;
    private String city;
    private String zipCode;

    // Constructor
    public Address(Long id, String street, String city, String zipCode) {
        this.id = id;
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }

    public String getZipCode() {
        return zipCode;
    }

    @Override
    public String toString() {
        return "Address{" +
                "id=" + id +
                ", street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        Address initialAddress = new Address(1L, "123 Main St", "Springfield", "12345");
        User user = new User(1L, "Alice",initialAddress);
        System.out.println(user);

        // Add another Order to the Customer
        Address secondAddress = new Address(2L, "123 Main St", "New Jersey", "12347585");

        user.addAddress(secondAddress);
        System.out.println(user);
    }
}

About

At DevelopersMonk, we share tutorials, tips, and insights on modern programming frameworks like React, Next.js, Spring Boot, and more. Join us on our journey to simplify coding and empower developers worldwide!

Email: developersmonks@gmail.com

Phone: +23359*******

Quick Links

  • Home
  • About
  • Contact Us
  • Categories

  • Java
  • TypeScript
  • JavaScript
  • React
  • Nextjs
  • Spring Boot
  • DevOps