Refactor to onion architecture

By Thomas Bracher

From Julie Moronuki

Goals

Explore options

Compare

Choose the best

Goals

  • Make the domain easy to test
  • Low coupling with infrastructure
  • Resilient to changes

N-Tier in theory

The legacy application

Use case

The ORM Way (or save it all)

Create a team with a new member

@Entity
class OrganizationOrm {
  @Id
  private long id;
  @OneToMany
  private List<TeamOrm> teams;

  public void createTeamAndMember() {
    val team = new TeamOrm();
    val member = new MemberOrm();
    team.add(member);
    this.teams.add(team);
  }
}

The ORM entities

ps: I never used an ORM, no guarantees it works

@Entity
class TeamOrm {
  @Id
  private long id;

  @ManyToMany
  private List<MemberOrm> members;
}
@Entity
class MemberOrm {
  @Id
  private long id;
}

Saving anything

class OrganizationRepository {
  private OrmRepository orm;

  public void save(OrganizationOrm organization) {
    this.orm.save(organization);
  }
}

Controller layer

class OrganizationController {
  @PutMapping("/organization/{orgaId}/team")
  public Response endpoint(String orgaId) {
    OrganizationOrm organization = repo.find(orgaId);
    organization.createTeamAndMember();
    repo.save(organization);
    return Response.ok();
  }
}

Any Comment?

@Entity
class OrganizationOrm {
  @Id
  private long id;
  @OneToMany
  private List<TeamOrm> teams;

  public void createTeamAndMember() {
    val team = new TeamOrm();
    val member = new MemberOrm();
    team.add(member);
    this.teams.add(team);
  }
}

Introduction of failure

(or else known as domain)

There can not be more than 10 teams per organization

@Entity
class OrganizationOrm {
  // ...

  public void createTeamAndMember() {
    if (teams.size() > 10) {
      return;
    }
    val team = new TeamOrm();
    val member = new MemberOrm();
    team.add(member);
    this.teams.add(team);
  }
}

What about the user?

De l'explicabilité des systèmes : les enjeux de l'explication des décisions automatisées

class OrganizationController {
  @PutMapping("/organization/{orgaId}/team")
  public Response endpoint(String orgaId) {
    OrganizationOrm organization = repo.find(orgaId);
    organization.createTeamAndMember();
    repo.save(organization);
    if (/* some condition */) {
      return Response.error();
    }
    return Response.ok();
  }
}

3 Fixes

public boolean createTeamAndMember()
public void createTeamAndMember() throws TooManyTeamsException
public Optional<TooManyTeams> createTeamAndMember()

Concurrent modification

It's the story of 10000 users editing the same organization but not only!

Strategies (for SQL)

  • Transactions? (no)
  • Pessimistic locking
  • Optimistic locking

Pessimistic locking

Use SELECT FOR UPDATE to lock the aggregate's row

The lock is freed on COMMIT or ROLLBACK

Optimistic locking

@Entity
class OrganizationOrm {
  // ...
  private int version;
}
class OrganizationRepository {
  private OrmRepository orm;

  public void save(OrganizationOrm organization) {
    this.orm.save(organization);
    this.orm.execute("update organisations " +
            " set version = $1 " +
            " where organisations.id = $2 " +
            " and organisations.version = $3",
      organisation.getVersion() + 1,
      organisation.getId(),
      organisation.getVersion());
  }
}

Wrap up the ORM way

  • Easy to test
  • Not far from classical architecture
  • Tedious in complex workflow
  • Rely on ORM or diff algorithms

The Event way, Granular saves

Domain

class Organization {
  private OrganizationId id;
  private List<Team> teams;

  public OrganizationEvent createTeamAndMember() {
    if (teams.size() > 10) {
      return new TooManyTeams();
    } else {
      return new AddNewTeamAndMember(this.id);
    }
  }
}

Controller layer

class OrganizationController {
  @PutMapping("/organization/{orgaId}/team")
  public Response endpoint(String orgaId) {
    Organization organization = repo.find(orgaId);
    val event = organization.createTeamAndMember();
    repo.save(event);
    return toResponse(event);
  }
}

Infrastructure

class OrganizationRepository {
  private OrmRepository orm;

  public void save(OrganizationEvent event) {
    if (event instanceOf AddNewTeamAndMember) {
      OrganizationOrm orga = this.orm.find(event.id);
      val team = new TeamOrm();
      val member = new MemberOrm();
      team.add(member);
      orga.getTeams.add(team);
      this.orm.save(orga);
    }
  }
}

Infrastructure (SQL)

class OrganizationRepository {
  private OrmRepository orm;

  public void save(OrganizationEvent event) {
    if (event instanceOf AddNewTeamAndMember) {
      long teamId = this.orm.get(
        "insert into teams (organization_id) values ($1) " +
        " returning id",
        event.id
      );
      this.orm.execute(
        "insert into members (team_id) values ($1)",
        teamId
      );
    }

What if event are propagated in the application

class OrganizationRepository {
  private OrmRepository orm;
  private EventNotifier notifier;

  public void save(OrganizationEvent event) {
    this.persist(event);
    this.notifier.propagate(event);
  }

Granular saves wrap up

  • Supple design
  • Immutability -> Easy Domain tests
  • Transaction & Lock still applicable
  • Infra coherence more difficult
  • Applicable to Application code

Conclusion

  • ORM is not the problem
  • Domain paradigm > Persistence
  • Testing is key