Oskar Dudycz

Pragmatic about programming

Twelve things I learned about Java during my last code review

2022-03-30 oskar dudyczCoding Life

As I have mentioned to you many times, I like to test different approaches, technologies and solutions. I may not have told you yet that I once wanted to be a Java developer. I became a .NET developer only because I couldn’t get any job as Java Developer. After long looking for a job, I was finally hired as a C# developer, and it stayed like that for a long time. I don’t regret it, as .NET moved forward, C# language evolved a lot, and Java stagnated at some point. I’m talking about the language because the community, especially the OSS, has always been strong and vibrant. I have always envied the maturity of Java tools, especially those for “enterprise” and production-ready solutions. From time to time, I participated in Java projects (probably the longest in Android), now I also contributed to the Java EventStoreDB client. I recently checked how my Java skills held up and did an Event Sourcing example.

I decided to port my C# sample. My main assumptions were:

  • explain the basics of Event Sourcing, both from the write model (EventStoreDB) and read model part (PostgreSQL and Spring Data JPA),
  • present that you can join the classical approach with Event Sourcing without making a massive revolution,
  • CQRS architecture is sliced by business features, keeping code that changes together simultaneously. Read more in How to slice the codebase effectively?.
  • Composable functions for command handlers, events, projections, query handling, minimising the need for marker interfaces. Thanks to that testability and easier maintenance.

The presented use case is Shopping Cart flow and is modelled as Web API written in Spring Boot and Java 17.

After I finished the initial version (https://github.com/oskardudycz/EventSourcing.JVM/pull/1), I decided to ask the “community” for a review. I wanted to make sure that what I did, was not just a dumb port but the code that’s idiomatic and similar to what typically is done in the Java World. That’s especially important, as I was doing so far mostly infrastructure code in Java, and business-related may differ from it. I wanted to get sharp but honest feedback.

Well, I got what I asked for!

cover

Okay, it wasn’t that bad, but it cost me “a bit” of work. And that’s awesome, as my code and my skill improved by that!

Let me share what I learned:

  1. In Java, the type Optional should only be used as the result of a method. It was created to reduce the void type confusion. It should not be used as a parameter or input field for a method. It makes sense because generics in Java are basically templates that exist before compilation (https://www.baeldung.com/java-generics). After that they’re just wiped out. See more about the initial Optional type design at: https://nipafx.dev/design-java-optional/.

  2. I’ve already used sealed interfaces to get a sweet pattern matching when rebuilding the state from events. However, it turned out that they allow the full use of union types! Thanks to the suggestions, I went further and created this shopping cart definition:

sealed public interface ShoppingCart {
  UUID id();
  UUID clientId();
  ProductItems productItems();

  record PendingShoppingCart(
    UUID id,
    UUID clientId,
    ProductItems productItems
  ) implements ShoppingCart {
  }

  record ConfirmedShoppingCart(
    UUID id,
    UUID clientId,
    ProductItems productItems,
    LocalDateTime confirmedAt
  ) implements ShoppingCart {
  }

  record CanceledShoppingCart(
    UUID id,
    UUID clientId,
    ProductItems productItems,
    LocalDateTime canceledAt
  ) implements ShoppingCart {
  }

  enum Status {
    Pending,
    Confirmed,
    Cancelled;
  }

  default boolean isClosed() {
    return this instanceof ConfirmedShoppingCart || this instanceof CanceledShoppingCart;
  }

  default ShoppingCart.Status status() {
    return switch (this) {
      case PendingShoppingCart pendingShoppingCart:
        yield Status.Pending;
      case ConfirmedShoppingCart confirmedShoppingCart:
        yield Status.Confirmed;
      case CanceledShoppingCart canceledShoppingCart:
        yield Status.Cancelled;
    };
  }

  static ShoppingCart when(ShoppingCart current, ShoppingCartEvent
    event) {
    return switch (event) {
      case ShoppingCartOpened shoppingCartOpened:
        yield new PendingShoppingCart(
          shoppingCartOpened.shoppingCartId(),
          shoppingCartOpened.clientId(),
          ProductItems.empty()
        );
      case ProductItemAddedToShoppingCart productItemAddedToShoppingCart:
        yield new PendingShoppingCart(
          current.id(),
          current.clientId(),
          current.productItems().add(productItemAddedToShoppingCart.productItem())
        );
      case ProductItemRemovedFromShoppingCart productItemRemovedFromShoppingCart:
        yield new PendingShoppingCart(
          current.id(),
          current.clientId(),
          current.productItems().remove(productItemRemovedFromShoppingCart.productItem())
        );
      case ShoppingCartConfirmed shoppingCartConfirmed:
        yield new ConfirmedShoppingCart(
          current.id(),
          current.clientId(),
          current.productItems(),
          shoppingCartConfirmed.confirmedAt()
        );
      case ShoppingCartCanceled shoppingCartCanceled:
        yield new CanceledShoppingCart(
          current.id(),
          current.clientId(),
          current.productItems(),
          shoppingCartCanceled.canceledAt()
        );
      case null:
        throw new IllegalArgumentException("Event cannot be null!");
    };
  }
}

See full sample in ShoppingCart.java. I miss such a possibility in C# because I believe that it enables concise and precise modelling of data structures.

  1. I’m a big fan of slicing architecture into vertical pieces (read more in my other article). I also believe that composition = simplicity. Nevertheless, “simplicity” means something different to each of us. Sometimes it is better to group smaller things to provide a more readable structure, such as the application service. Instead of going into many tiny slices, I decided to add ShoppingCartService that shows what’s possible to do with Shopping Cart. It made things clearer, especially for newcomers trying to understand my sample.

  2. Spring Boot has a built-in ApplicationEventPublisher, so you don’t need to build your event bus in memory. I had to go full circle and remove my “great” but totally redundant implementation. Remember: learn the tools before moving on to custom, custom implementations.

  3. Java requires defining exceptions explicitly that our method can throw. However, there is no point in pushing up all possible exceptions. It is like being holier than the Pope. If you don’t want to handle them in your code, it’s better to make them runtime exceptions and catch them in a global mechanism. Read more about the breakdown of exceptions to checked/unchecked: https://baeldung-cn.com/java-checked-unchecked-exceptions.

  4. The ControllerAdvice and ExceptionHandler annotations can be used to handle global exception handling. They enable automatic mapping of exceptions to HTTP statuses; see how I did that in GlobalExceptionHandler.java.

  5. Since I’m not a big fan of annotations/attributes/decorators, I manually register dependencies as Java Beans. I stand by my decision, but I think I have gone too far. It is better to compose stateless code than to delegate it to DI.

  6. The reviewers also motivated me to finish what I started instead of postponing it to additional Pull Requests. For example, thanks to their comments, I came up with a nice test syntax for Given / When / Then API:

@SpringBootTest(classes = ECommerceApplication.class,
  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AddProductItemToShoppingCartTests extends ApiSpecification {
  public final UUID clientId = UUID.randomUUID();
  private UUID shoppingCartId;
  private ETag eTag;

  public AddProductItemToShoppingCartTests() {
    super("api/shopping-carts");
  }

  @BeforeEach
  public void openShoppingCart() {
    var result =
      ShoppingCartRestBuilder.of(restTemplate, port)
        .build(cart -> cart.withClientId(clientId));

    shoppingCartId = result.id();
    eTag = result.eTag();
  }

  @Test
  public void addProductItem_succeeds_forValidDataAndExistingShoppingCart() {
    given(() ->
      new AddProduct(new ProductItemRequest(
        UUID.randomUUID(),
        2
      )))
      .when(POST("%s/products".formatted(shoppingCartId), eTag))
      .then(OK);
  }

  @Test
  public void addProductItem_succeeds_forValidDataAndNonEmptyExistingShoppingCart() {
    var result =
      ShoppingCartRestBuilder.of(restTemplate, port)
        .build(cart -> cart
          .withClientId(clientId)
          .withProduct(new ProductItemRequest(UUID.randomUUID(), 10))
        );

    given(() ->
      new AddProduct(new ProductItemRequest(
        UUID.randomUUID(),
        2
      )))
      .when(POST("%s/products".formatted(result.id()), result.eTag()))
      .then(OK);
  }

  // (...)
}

Check more in:

I even got thrilled about how cool is that! I think that I’ll port that to my C# samples.

  1. Instead of manually blocking threads, waiting with Thread.sleep to ensure the EventStoreDB subscription restarted, I found a spring Retry mechanism (https://docs.spring.io/spring-batch/docs/current/reference/html/retry.html). I used it to build a retry policy: EventStoreDBSubscriptionToAll.java. Again, throwing away the handcrafted code helped me improve the solution.

  2. I have also used Spring Boot Retry to implement long-polling. It is a technique that allows us to simulate the synchronisation of the API with eventual consistency at the data recording level. The If-None-Match header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) can help with this. See more in an example: ShoppingCartService.java.

  3. I also added Log4J instead of System.out.println for logs. Yes, Log4J. By the way, I believe that the famous Log4J security issue was not a Log4J problem. The problem would not reach this scale if companies (especially big corporations) had an appropriate dependency management strategy. Anyone can be breached, primarily such a popular tool, but we must be prepared for it as architects.

  4. I also added a Continuous Integration process using Docker images for EventStoreDB and Postgres to run end-to-end integration tests. We don’t live in caves anymore. You can run tests on real databases. See more: samples_event-sourcing-esdb-simple.yml.

Finally, some numbers:

  • 143 comments in discussions in PR,
  • 47 commits,
  • I changed 80 files,
  • 3906 lines of code.

It’s been a long way! But it was worth it, and thanks to the community’s help, I eventually came up with a much sounder solution. Plus, I was able to share with you what I have learned. I hope that it may save you some mistakes. You don’t have to learn on your own constantly!

Cheers.

Oskar

p.s. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.

👋 If you found this article helpful and want to get notification about the next one, subscribe to Architecture Weekly.

✉️ Join over 11500 subscribers, get the best resources to boost your skills, and stay updated with Software Architecture trends!

Loading...
Event-Driven by Oskar Dudycz

cover

Through my window, I see the result of good plans but poor execution. Opposite my flat, there is a partially completed construction place. Buildings were supposed to be eye-catching Mediterranean style apartments. Delivery date? Two years ago. Actual? More and more unknown.

Some time ago, I heard that using Event Sourcing makes creating Event-Driven Architecture easier. The arguments were correct, that if we’re already publishing events to trigger business workflows, then at some point, we may want to also store events to not lose information. Agreed. However, I also heard that keeping the state as events will simplify things. We’ll have a source of truth with a record of the system behaviour. This will allow, e.g. to confront the results of the operations with the recorded state. I’d agree with that, with one distinction. It’s easier as long as you already know Event Sourcing.

Many people in the DDD community claim that the essential is to properly break down the system into autonomous parts called bounded contexts. Once we have it, the rest is secondary and will sort itself out. For sure.

Many seasoned programmers speak similarly about new technologies. They claim that they can translate past experience into new technologies. That’s true that by analogy, they can catch the big picture quicker. But isn’t it a bold assumption to say that Win.Forms specialist will learn Angular quickly?

The end result may differ a lot from the initial ideas. I saw the plan of those buildings next to me. Now I can see the effects of the execution. Or actually, the lack.

I believe that we should carefully acknowledge not only the point of view of our authorities but also their seating point. If we want to find out how to form a wall, do we ask an architect or a foreman? An architect may know the theory, but the practice is what we’re looking for. On the other hand, if you want to know where to put the wall, you prefer the architect to do measurements. At least if you don’t want to have the roof falling to your head.

After I had torn a ligament in my knee, I went to two qualified orthopedists. One said I should have surgery and do a reconstruction. The second stated that there is no need for that; rehabilitation should be enough. Guess which one had a specialization in surgery and which in rehabilitation?

People usually give us advice from the point where they’re currently standing. They are entitled to a biased view. An architect who rarely does programming will tend to downplay the value of implementation and tactical patterns. Midlevel developers will focus on technicalities instead of the global system impact. The team manager or consultant will emphasize the importance of soft skills (or esoteric techniques known only to them).

The truth is that we need all of them. The excellent plan will fall on the bad execution. The best execution for the wrong case will be just a waste of time. We should carefully evaluate the advice considering what we need and what an expert can give us.

Therefore, when we’re reading an article, watching a talk, let’s also pay attention to the place where the person is standing. The perspective from there may be much different from where we are right now. That can be good, as it may push us in the right direction. But it may also be misleading, as we accidentally take biases of this person without understanding the tradeoffs. Personally, I prefer to follow not only people from pedestal but also those that are closer to my position. A bit further in the journey, but not too far. That helps me to calibrate my view as those people are more relative to my daily struggles.

Polish historical leader Józef Piłsudzki reportedly used to say: “Right is like an ass, everyone has its own”.

Cheers!

Oskar