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.
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.
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 }
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:
An association denotes the relationship between two or more entities. It describes how instances relating to one entity are related to instances of another.
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{ }
A unidirectional association may be 1-0..1, 1-1, or one-many
One-one Multiplicity:
Ownership:
One entity typically "owns" the relationship. For example, Entity A may own Entity B, meaning Entity B cannot exist without Entity A.
The same rules apply to the User and Profile example:
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:
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); } }
General Rules for Unidirectional One-to-Many Multiplicity
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 + '\'' + '}'; } }
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
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); } }
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!