I tried to make a sample program using the problem of database specialist in Domain Driven Design

Introduction

Nice to meet you. My name is Kotobukiyama. This is Qiita's first post! !!

Do you guys do Domain Driven Design? If you do Domain Driven Design, you can write a program with high maintainability, so I want to try it! !! It will be. But it's difficult, isn't it? Domain Driven Design ... There are several books, but they are difficult to understand. Many people may want to see more practical code samples. However, in recent years, domain-driven design has become a hot topic at study sessions and events, and I feel that the opportunities to see sample programs are increasing. Therefore, I wanted to write a sample program myself and acquire practical know-how, so I made a sample program with the exam questions of IPA database specialists as the theme.

The source code is here The version at the time of posting this article is tag 1.0.

It wasn't a domain-driven design at all, so it seemed like a lot of tsukkomi would come in, but I managed to get it into shape. This time, I would like to talk about what was good and what didn't work when I actually tried it.

Application specifications, etc.

problem

IPA 2019 Spring Database Specialist Exam This is a sample program created by domain-driven design of the tournament management system that was asked in question 1 of 1 pm question.

PDF download page in question

Limitations

ER diagram

The table design is the same as the question that was asked.

er diagram.png

Class diagram

I extracted only the classes of the domain model and created a class diagram. To avoid cluttering the lines, I've written few associations. Since the Entry class is an abstract class, only the relation of generalization of the concrete class that implements it and the multiplicity of the MemberPoints class and the MemberPoint class are described. I wrote the class diagram later, but there are few methods ... (Is this really what we call Domain Driven Design ...)

class diagram.png

API endpoint

This time, I created the following three endpoints.

Apply for entry slot

POST : /applications/entry

Register the lottery result

POST : /lottery-entry-result

Make a deposit

POST : /applications/payment

What was good and what wasn't

Application layer is bloated

If you could create a method in the domain model, the Application layer would be ** relatively thin, and if you could hardly create a method in the domain model, the Application layer would be bloated. (It is a natural result ...)

The methods of the Application layer of the three endpoints are as follows

ApplicationCommandService.java


  /**
   *Apply for entry slot.
   */
  public void applyForEntry(ApplyForEntryRequest request) {

    final FestivalId festivalId = request.festivalId();
    final MemberId memberId = request.memberId();
    final EntryId entryId = request.entryId();
    final LocalDate applicationDate = request.getApplicationDate();

    final Member member = memberRepository.findMember(request.memberId());
    if (member == null) {
      throw new BusinessErrorException("It is a member that does not exist");
    }

    final Application alreadyApplication =
        applicationRepository.findApplication(festivalId, memberId);

    if (alreadyApplication != null) {
      throw new BusinessErrorException("I have already applied for the specified tournament");
    }

    final Entry entry = entryRepository.findEntry(festivalId, entryId);

    if (entry == null) {
      throw new BusinessErrorException("It is an entry frame that does not exist");
    }

    final Application application = Application.createEntityForEntry(
        festivalId,
        memberId,
        entryId,
        applicationDate
    );

    entry.validateAndThrowBusinessErrorIfHasErrorForApplication(application);

    entry.incrementApplicationNumbers();
    entryRepository.saveEntry(entry);

    applicationRepository.addApplication(application);
  }

LotteryEntryResultCommandService.java


  /**
   *Register the lottery result.
   */
  public void registerLotteryEntryResult(RegisterLotteryEntryResultRequest request) {

    final Member member = memberRepository.findMember(request.memberId());
    if (member == null) {
      throw new BusinessErrorException("It is a member that does not exist");
    }

    final Entry entry = entryRepository.findEntry(request.festivalId(), request.entryId());

    if (entry == null) {
      throw new BusinessErrorException("It is an entry frame that does not exist");
    }

    if (!entry.isLotteryEntry()) {
      throw new BusinessErrorException("It is not a lottery entry frame");
    }

    if (entry.entryStatus() != EntryStatus.underLottery) {
      throw new BusinessErrorException("The lottery has not started yet");
    }

    LotteryEntryResult already = lotteryEntryResultRepository.findLotteryEntryResult(
        request.festivalId(),
        request.memberId(),
        request.entryId()
    );
    if (already != null) {
      throw new BusinessErrorException("The lottery result has already been registered");
    }

    Application application = applicationRepository.findApplication(
        request.festivalId(),
        request.memberId());
    if (application == null) {
      throw new BusinessErrorException("I have not applied for the target tournament");
    }

    //If it is not the entry slot for which you applied, is it a multi-stage lottery slot for the entry slot you applied for?
    if (!(application.entryId().equals(request.entryId()))) {
      Entry firstEntry = entryRepository.findEntry(request.festivalId(), application.entryId());
      if (!(((LotteryEntry)firstEntry).followingEntryId().equals(request.entryId()))) {
        throw new BusinessErrorException("It is an entry frame that is not subject to the lottery");
      }

      LotteryEntryResult firstEntryResult =
          lotteryEntryResultRepository.findLotteryEntryResult(
              application.festivalId(),
              application.memberId(),
              application.entryId());
      if (firstEntryResult == null) {
        throw new BusinessErrorException("The lottery result of the entry frame you applied for has not been registered yet");
      }

      if (firstEntryResult.lotteryResult() == LotteryResult.winning) {
        throw new BusinessErrorException("Since you have already won, you cannot register the lottery result for the subsequent entry slot.");
      }
    }

    LotteryEntryResult lotteryEntryResult = new LotteryEntryResult(
        request.festivalId(),
        request.memberId(),
        request.entryId(),
        request.getLotteryResult()
    );

    lotteryEntryResultRepository.saveLotteryEntryResult(lotteryEntryResult);
  }

PaymentCommandService.java


  /**
   *Make a deposit.
   */
  public void payToApplication(PaymentRequest request) {

    final FestivalId festivalId = request.festivalId();
    final MemberId memberId = request.memberId();

    Application application = applicationRepository.findApplication(festivalId, memberId);

    if (application == null) {
      throw new BusinessErrorException("I have not applied for the target tournament");
    }

    Entry entry = entryRepository.findEntry(festivalId, application.entryId());
    if (entry.isLotteryEntry()) {
      //If the target entry is a lottery, check if it has won
      LotteryEntryResult entryResult = lotteryEntryResultRepository.findLotteryEntryResult(
          festivalId, memberId, entry.entryId());

      if (entryResult.lotteryResult() == LotteryResult.failed) {
        EntryId followingEntryId = ((LotteryEntry)entry).followingEntryId();
        if (followingEntryId == null) {
          throw new BusinessErrorException("I have not won the target tournament");
        } else {
          LotteryEntryResult followingEntryResult =
              lotteryEntryResultRepository.findLotteryEntryResult(
                  festivalId, memberId, followingEntryId);

          if (followingEntryResult.lotteryResult() == LotteryResult.failed) {
            throw new BusinessErrorException("I have not won the target tournament");
          }
        }
      }
    }

    PointAmount usePoints = new PointAmount(request.getUsePoints());
    if (usePoints.isPositive()) {
      MemberPoints memberPoints = memberPointRepository.findMemberPoints(memberId);
      memberPoints.usePoints(request.getPaymentDate(), usePoints);

      memberPointRepository.saveMemberPoints(memberPoints);
    }

    application.pay(request.getPaymentDate(), usePoints);
    applicationRepository.saveApplication(application);
  }

I think that "applying for entry frame" and "paying" are still better, but "registering lottery results" has resulted in creating a lot of business error check processing in the Application layer. I wondered if I should create a domain service at such times, but I stopped this time because it seemed that just creating a domain service class would just change the place to write the code. In the first place, I feel that the design of Entity is not working well due to the DB.

For those who "apply for entry" and "deposit", I feel that I was able to design a little object-oriented, so let me introduce you.

Apply for entry slot

There are patterns of first-come-first-served frame and lottery frame in the entry frame, but this time, I created the entry frame with an abstract class and made the first-come-first-served frame and the lottery frame a concrete class.

For example, when applying, the number of applicants is incremented, but each has the following implementation.

FirstArrivalEntry.java


/**First-come-first-served entry frame. */

  /**
   *Increment the number of applicants.When the number of applicants reaches the capacity, change to confirm participants.
   */
  @Override
  public void incrementApplicationNumbers() {
    applicationNumbers = applicationNumbers.increment();

    if (capacity.same(applicationNumbers)) {
      entryStatus = EntryStatus.participantConfirmation;
    }
  }

LotteryEntry.java


/**Lottery entry frame. */

  @Override
  public void incrementApplicationNumbers() {
    applicationNumbers = applicationNumbers.increment();
  }

In the case of the first-come-first-served basis, it is necessary to close the application when the number of applicants reaches the capacity. On the client side, the method is executed for the interface as shown below, so I wonder if it could be object-oriented.

ApplicationCommandService.java


final Entry entry = entryRepository.findEntry(festivalId, entryId);
entry.incrementApplicationNumbers();

By the way, in the part that acquires Entity from DB, data is acquired from MyBatis by DTO, and the classes to be generated are divided according to the first-come-first-served frame or the lottery frame. I wonder if this is also the part that worked.

MybatisEntryRepository.java


  public Entry findEntry(FestivalId festivalId, EntryId entryId) {

    EntryDto dto = entryMapper.selectEntry(festivalId, entryId);

    if (dto == null) {
      return null;
    }

    if (dto.firstArrivalLotteryType == FirstArrivalLotteryType.firstArrival) {

      return new FirstArrivalEntry(
          dto.festivalId,
          dto.entryId,
          dto.entryName,
          dto.entryDescription,
          dto.eventCode,
          dto.capacity,
          dto.participationFees,
          dto.applicationNumbers,
          dto.applicationStartDate,
          dto.applicationEndDate,
          dto.entryStatus);
    } else {

      return new LotteryEntry(
          dto.festivalId,
          dto.entryId,
          dto.entryName,
          dto.entryDescription,
          dto.eventCode,
          dto.capacity,
          dto.participationFees,
          dto.applicationNumbers,
          dto.applicationStartDate,
          dto.applicationEndDate,
          dto.entryStatus,
          dto.lotteryDate,
          dto.followingEntryId);
    }
  }

Make a deposit

You can use points when depositing, but this point has an expiration date of one year, and it is a specification that the one with the closest expiration date will be used, so I think that this worked well.

I am using the collection objects introduced in Principles of system design useful in the field.

MemberPoint.java


public class MemberPoint implements Entity {

  private MemberId memberId;

  private LocalDate givenPointDate;

  private PointAmount givenPoint;

  private PointAmount usedPoint;

  /**
   *Use points for the value used in the argument.
   */
  void use(BigDecimal value) {
    BigDecimal totalUsedPointAmountValue = usedPoint.value().add(value);

    if (totalUsedPointAmountValue.compareTo(givenPoint.value()) > 0) {
      throw new IllegalArgumentException("There are more used points than granted points");
    }

    usedPoint = new PointAmount(totalUsedPointAmountValue);
  }
}

MemberPoints.java


public class MemberPoints {

  private List<MemberPoint> list;

  /**
   *Determine if the argument points can be used and use from the point near the expiration date.
   *In addition, change the state of the MemberPoint object to be retained.
   */
  public void usePoints(LocalDate paymentDate, PointAmount pointAmount) {

    //Use points from the points given so far until this value becomes zero
    BigDecimal x = pointAmount.value();

    for (MemberPoint memberPoint : list) {

      //Expiration date check
      LocalDate expirationDate = memberPoint.givenPointDate().plusYears(1);
      if (paymentDate.compareTo(expirationDate) > 0) {
        continue;
      }

      //Check the point balance
      BigDecimal availableUsePoint = memberPoint.givenPoint().value()
          .subtract(memberPoint.usedPoint().value());
      if (availableUsePoint.compareTo(BigDecimal.ZERO) == 0) {
        continue;
      }

      if (availableUsePoint.compareTo(x) <= 0) {
        memberPoint.use(availableUsePoint);
        x = x.subtract(availableUsePoint);
      } else {
        memberPoint.use(x);
        x = BigDecimal.ZERO;
        break;
      }
    }

    //Use points from the one with the closest expiration date, and if the number of points you want to use is not reached, an error will occur.
    if (x.compareTo(BigDecimal.ZERO) > 0) {
      throw new BusinessErrorException("Insufficient points");
    }
  }
}

From the client, I was able to check the point balance and change the status of multiple Member Points with the following code.

MemberPoints memberPoints = memberPointRepository.findMemberPoints(memberId);
memberPoints.usePoints(request.getPaymentDate(), usePoints);
memberPointRepository.saveMemberPoints(memberPoints);

at the end

In the end, it became a reasonable volume, and I'm glad that I was able to create a relatively practical sample, including validation and saving to the DB. However, what I actually tried and wanted to improve is as follows.

In order to improve these, I think I'll try refactoring or practicing with problems of different years (maybe I should have chosen a problem with more calculations).

Also, if you read this article and have an opinion such as "Isn't it better if you do this?", I would appreciate it if you could comment. Thank you for reading this far! !!

Recommended Posts

I tried to make a sample program using the problem of database specialist in Domain Driven Design
Creating a sample program using the problem of a database specialist in DDD Improvement 2
Creating a sample program using the problem of a database specialist in DDD Improvement 1
I tried to make a client of RESAS-API in Java
I tried to make the sample application into a microservice according to the idea of the book "Microservice Architecture".
I tried to make the "Select File" button of the sample application created in the Rails tutorial cool
I tried to make a parent class of a value object in Ruby
I tried to make full use of the CPU core in Ruby
I tried to make a talk application in Java using AI "A3RT"
I made a sample of how to write delegate in SwiftUI 2.0 using MapKit
I tried using a database connection in Android development
[Ruby] I want to make a program that displays today's day of the week!
I stumbled when I tried using neo4j in the jenv environment, so make a note
I tried using Hotwire to make Rails 6.1 scaffold a SPA
I tried to solve the problem of "multi-stage selection" with Ruby
I tried to illuminate the Christmas tree in a life game
[Unity] I tried to make a native plug-in UniNWPathMonitor using NWPathMonitor
[Java] I tried to make a maze by the digging method ♪
I tried to solve the problem of Google Tech Dev Guide
[Rails] Implementation of multi-layer category function using ancestry "I tried to make a window with Bootstrap 3"
After learning Progate, I tried to make an SNS application using Rails in the local environment
I tried a calendar problem in Ruby
I tried to make a simple face recognition Android application using OpenCV
I tried to summarize the key points of gRPC design and development
I tried to solve the tribonacci sequence problem in Ruby, with recursion.
Sample program that returns the hash value of a file in Java
Make a margin to the left of the TextField
I tried to organize the session in Rails
What I tried when I wanted to get all the fields of a bean
I tried to solve the tribonatch sequence problem in Ruby (time limit 10 minutes)
I wanted to make (a == 1 && a == 2 && a == 3) true in Java
I tried to make a program that searches for the target class from the process that is overloaded with Java
How to make a unique combination of data in the rails intermediate table
[Small story] I tried to make the java ArrayList a little more convenient
I tried to implement a server using Netty
I tried using the profiler of IntelliJ IDEA
I tried to make a web application that searches tweets with vue-word cloud and examines the tendency of what is written in the associated profile
[Swift] I already have a lot of information, but I tried to summarize the cast (as, as !, as?) In my own way.
I tried to make a product price comparison tool of Amazon around the world with Java, Amazon Product Advertising API, Currency API (2017/01/29)
I tried to create a log reproduction script at the time of apt install
[Beginner's point of view] I tried to solve the FizzBuzz problem "easily" with Ruby!
I tried to make a message function of Rails Tutorial extension (Part 1): Create a model
[Rails] I want to display the link destination of link_to in a separate tab
I tried to investigate the mechanism of Emscripten by using it with the Sudoku solver
Sample code to assign a value in a property file to a field of the expected type
I tried to organize the cases used in programming
I tried using the Server Push function of Servlet 4.0
I tried to summarize the state transition of docker
05. I tried to stub the source of Spring Boot
I tried to reduce the capacity of Spring Boot
I tried to create a Clova skill in Java
I wrote a sequence diagram of the j.u.c.Flow sample
Using the database (SQL Server 2014) from a Java program 2018/01/04
I tried to implement the Euclidean algorithm in Java
I built an environment to execute unit tests using Oracle database (oracle12c) on the Docker in Docker (dind) image of GitLab-CI.
[VBA] I tried to make a tool to convert the primitive type of Entity class generated by Hibernate Tools to the corresponding reference type.
I finished watching The Rose of Versailles, so I tried to reproduce the ending song in Java
I tried to make a reply function of Rails Tutorial extension (Part 3): Corrected a misunderstanding of specifications
Sample code to get the values of major SQL types in Java + Oracle Database 12c
I tried to solve the Ruby bonus drink problem (there is an example of the answer)
I tried to touch the asset management application using the emulator of the distributed ledger Scalar DLT