I recently worked on updating our backend application to adhere to hexagonal architecture principles. It was a bit of a challenge at times since there weren’t any set rules about what the API exposed, but it provided me with an opportunity to propose some initial guidelines myself for better organization and readability.
As the only Java developer within a C# team, I often face criticism regarding following best practices or the perceived unreadability of Java code. Given my multi-year experience in the field, I believe that many of these concerns stem from differences in preference between Java and C# developers or a lack of proficiency in Java. To keep things simple and readable, I’m seeking feedback from experienced Java developers.
Currently, I’m delving into Functional Design…
I recently worked on updating our backend application to adhere to hexagonal architecture principles. It was a bit of a challenge at times since there weren’t any set rules about what the API exposed, but it provided me with an opportunity to propose some initial guidelines myself for better organization and readability.
As the only Java developer within a C# team, I often face criticism regarding following best practices or the perceived unreadability of Java code. Given my multi-year experience in the field, I believe that many of these concerns stem from differences in preference between Java and C# developers or a lack of proficiency in Java. To keep things simple and readable, I’m seeking feedback from experienced Java developers.
Currently, I’m delving into Functional Design: Principles, Patterns, and Practices by Uncle Bob. Inspired by this book, I have proposed creating an ArchUnit rule that mandates all newly created domain models to be Java records, which provide immutability and less boilerplate code via the automatic implementation of toString(), equals(), and hashCode() methods.
Upon having this idea challenged by various LLMs, we (the LLMs and I) have concluded that these models should be decorated with “Wither” methods (Lombok’s @With annotation) to minimize the need for instantiating the record with all its fields when only a single value is modified. For example, when changing the status, this approach reduces boilerplate code and lowers the risk of errors.
However, there were some challenges in making “Wither” methods work effectively. During our team meeting, we identified potential issues, which I will explain using a simplified code example:
@With
public record Bandwidth(int lowerLimit, int upperLimit) {
public Bandwidth {
if (lowerLimit > upperLimit) {
throw new IllegalArgumentException("lowerLimit must not be less or
equal than upperLimit");
}
}
public Bandwidth setNewLimits(int lowerLimit, int upperLimit) {
return this.withLowerLimit(lowerLimit).withUpperLimit(upperLimit);
}
}
When I run the following test, it throws an IllegalArgumentException, even though the object’s state would finally pass the business rule, which dictates that the upper limit should be greater than the lower limit.
@Test
void updateLimits_createsNewInstance(){
var bandwidth = new Bandwidth(10, 20);
var result = bandwidth(30, 50);
assertThat(result).isNotEqualTo(bandwidth); //stupid assertion, but let’s run with it for now
}
The IllegalArgumentException is thrown because of the intermediate new instance created by .withLowerLimit, which hasn’t yet updated the upper limit. To address this issue, I considered adding another rule: if more than one instance variable of the object is being updated, it should only happen through one new instantiation, not chained Wither methods.
But fundamentally, this raised the question: should we abandon the idea of simplifying domain model instantiation and remove the @With annotation? Or should we take a different approach by defining the @Builder annotation with “toBuilder=true” instead? This would allow us to implement setNewLimits as follows:
public Bandwidth setNewLimits(int lowerLimit, int upperLimit) {
return this.toBuilder()
.lowerLimit(lowerLimit)
.upperLimit(upperLimit)
.build();
}
However, I do not appreciate that updating a single attribute might lead to more boilerplate code with this approach. I’m eager to hear your thoughts on this and receive feedback from other experienced engineers, as I am certain I’m not the first person to encounter this challenge.