$30 off During Our Annual Pro Sale. View Details »

Just the facts

Just the facts

Presented to Working Software in Milan on 30 June 2023

Sara Pellegrini presented an interesting application design problem in her Kill Aggregate series of articles, where she argued that some of the accepted conventions in the event sourcing design style are not good enough to solve it. In this presentation we review some of the fundamentals patterns that are part of the context of Sara's argument, and present a simple alternative way to look at it

Matteo Vaccari

July 06, 2023
Tweet

More Decks by Matteo Vaccari

Other Decks in Programming

Transcript

  1. © 2023 Thoughtworks | Confidential
    Just the facts
    Matteo Vaccari Working Software, 30/06/2023

    View Slide

  2. © 2023 Thoughtworks | Confidential
    Business rule #1
    “A student may enroll in a maximum of 10 courses”
    2

    View Slide

  3. © 2023 Thoughtworks | Confidential 3
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    if (enrollments == MAX_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    }
    This is how the problem
    could be solved with
    today’s technology…
    and the style of the 60’s

    View Slide

  4. © 2023 Thoughtworks | Confidential 4
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    if (enrollments == MAX_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    private long findCountOfEnrollments(long studentId) {
    String query = """
    select count(*) from student_course_enrollments
    where student_id = ?
    and status = ‘active’
    """;
    return jdbcTemplate.queryForObject(query, (rs, __) -> rs.getLong(1), studentId);
    }
    private static class EnrollmentLimitExceeded extends RuntimeException {}
    }
    This is how the problem
    could be solved with
    today’s technology…
    and the style of the 60’s

    View Slide

  5. © 2023 Thoughtworks | Confidential 5
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    if (enrollments == MAX_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    private long findCountOfEnrollments(long studentId) {
    String query = """
    select count(*) from student_course_enrollments
    where student_id = ?
    and status = ‘active’
    """;
    return jdbcTemplate.queryForObject(query, (rs, __) -> rs.getLong(1), studentId);
    }
    private void insertNewEnrollment(long studentId, long courseId) {
    String updateQuery =
    "insert into student_course_enrollments (student_id, course_id) VALUES (?, ?)";
    jdbcTemplate.update(updateQuery, studentId, courseId);
    }
    private static class EnrollmentLimitExceeded extends RuntimeException {}
    }
    This is how the problem
    could be solved with
    today’s technology…
    and the style of the 60’s

    View Slide

  6. © 2023 Thoughtworks | Confidential
    “Transaction Script”
    A Transaction Script organizes all this logic primarily as a
    single procedure, making calls directly to the database
    or through a thin database wrapper.
    Each transaction will have its own Transaction Script
    6
    https://martinfowler.com/eaaCatalog/transactionScript.html

    View Slide

  7. © 2023 Thoughtworks | Confidential
    Business rule #2
    “Students may change their city of residence”
    (“... and the new city must be a different city from the old one”)
    7

    View Slide

  8. © 2023 Thoughtworks | Confidential 8
    @RestController
    public class ChangeCityOfResidenceController {
    private final JdbcTemplate jdbcTemplate;
    public ChangeCityOfResidenceController(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
    }
    @PutMapping("/student/city-of-residence")
    public void changeCityOfResidence(StudentChangeCityOfResidenceRequest request) {
    City previousCity = jdbcTemplate.queryForObject(
    "select city_of_residence_id from students where id = ?",
    (rs, rowNum) -> City.of(rs.getInt("city_of_residence_id")),
    request.studentId()
    );
    if (previousCity.equals(request.cityId())) {
    throw new CityOfResidenceMustBeChangedException();
    }
    String updateQuery =
    "update students set city_of_residence_id = ?";
    jdbcTemplate.update(updateQuery, request.studentId(), request.cityId());
    }
    private static class CityOfResidenceMustBeChangedException extends RuntimeException {}
    }
    Another business
    operation, another
    transaction script.

    View Slide

  9. © 2023 Thoughtworks | Confidential
    There must be a better way
    9

    View Slide

  10. © 2023 Thoughtworks | Confidential
    “Domain Model”
    An object model of the domain that incorporates both
    behavior and data.
    … business logic can be very complex. Rules and logic
    describe many different cases and slants of behavior,
    and it's this complexity that objects were designed to
    work with.
    10
    https://martinfowler.com/eaaCatalog/domainModel.html

    View Slide

  11. © 2023 Thoughtworks | Confidential 11
    public class Student {
    private static final int MAX_COURSE_ENROLLMENTS = 10;
    private List enrolledCourses;
    private City cityOfResidence;
    public Student(List courses, City cityOfResidence) {
    this.enrolledCourses = courses;
    this.cityOfResidence = cityOfResidence;
    }
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS) {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) throws CityOfResidenceMustBeChanged {
    if (cityOfResidence.equals(newCityOfResidence)) {
    throw new CityOfResidenceMustBeChanged();
    }
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {}
    private static class CityOfResidenceMustBeChanged extends DomainException {}
    }
    The domain model holds all
    relevant data to implement
    business rules,
    with none of the persistence

    View Slide

  12. © 2023 Thoughtworks | Confidential
    Heuristics for aggregate design
    “Aggregates should be small”
    12

    View Slide

  13. © 2023 Thoughtworks | Confidential 13
    public class Student {
    private static final int MAX_COURSE_ENROLLMENTS = 10;
    private List enrolledCourses;
    private City cityOfResidence;
    public Student(List courses, City cityOfResidence) {
    this.enrolledCourses = courses;
    this.cityOfResidence = cityOfResidence;
    }
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS) {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) throws CityOfResidenceMustBeChanged {
    if (cityOfResidence.equals(newCityOfResidence)) {
    throw new CityOfResidenceMustBeChanged();
    }
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {}
    private static class CityOfResidenceMustBeChanged extends DomainException {}
    }
    What do these two
    operations have in
    common?

    View Slide

  14. © 2023 Thoughtworks | Confidential
    Domain modelling is an iterative process
    14
    Student
    Student_Personal_Info
    Enrollment
    has many
    city_of_residence
    There’s no interaction
    between enrollments
    and city of residence
    So we model two
    aggregates

    View Slide

  15. © 2023 Thoughtworks | Confidential 15
    Business rule #3
    “Students who are resident in Rome
    can enroll in “Roman history” and this
    does not count against the limit of
    max 10 enrollments”

    View Slide

  16. © 2023 Thoughtworks | Confidential 16
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    if (enrollments == MAX_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    }
    TRANSACTION SCRIPT

    View Slide

  17. © 2023 Thoughtworks | Confidential 17
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    if (enrollments == MAX_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    }
    TRANSACTION SCRIPT

    View Slide

  18. © 2023 Thoughtworks | Confidential 18
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    int effectiveMaxEnrollments = MAX_ENROLLMENTS;
    if (enrollments == effectiveMaxEnrollments) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    }
    TRANSACTION SCRIPT

    View Slide

  19. © 2023 Thoughtworks | Confidential 19
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    int effectiveMaxEnrollments = MAX_ENROLLMENTS;
    if (enrollments == effectiveMaxEnrollments) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    }
    TRANSACTION SCRIPT

    View Slide

  20. © 2023 Thoughtworks | Confidential 20
    @RestController
    public class CourseEnrollmentController {
    @PostMapping("/student/courses")
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    long enrollments = findCountOfEnrollments(request.studentId());
    int effectiveMaxEnrollments = MAX_ENROLLMENTS;
    if (cityOfResidence(request.studentId()) == ROME && request.courseId() == ROMAN_HISTORY) {
    effectiveMaxEnrollments = MAX_ENROLLMENTS + 1;
    }
    if (enrollments == effectiveMaxEnrollments) {
    throw new EnrollmentLimitExceeded();
    }
    insertNewEnrollment(request.studentId(), request.courseId());
    }
    }
    TRANSACTION SCRIPT

    View Slide

  21. © 2023 Thoughtworks | Confidential 21
    Business rule #3, done!
    👍

    View Slide

  22. © 2023 Thoughtworks | Confidential 22
    Business rule #3, done!
    👍
    Or…. did we miss something? 🤨

    View Slide

  23. © 2023 Thoughtworks | Confidential
    Failure scenario
    23
    given a student who is resident in Rome
    and is enrolled in 11 courses, including “Roman history”
    when the student changes residence to Milan
    then ???

    View Slide

  24. © 2023 Thoughtworks | Confidential
    Failure scenario
    24
    given a student who is resident in Rome
    and is enrolled in 11 courses, including “Roman history”
    when the student changes residence to Milan
    then ???
    A conversation with domain
    experts should clarify what
    should happen.
    Some possibilities:
    ● The change should fail
    ⟶ the existing rules are enforced, OR
    ● The change is permitted
    ⟶ this is a new business rule, OR
    ● We no longer think of changing residence as an
    instantaneous transaction, it becomes a process
    ⟶ we improve our model of the business process

    View Slide

  25. © 2023 Thoughtworks | Confidential
    Transaction scripts failed us!
    25

    View Slide

  26. © 2023 Thoughtworks | Confidential 26
    public class Student {
    private List enrolledCourses;
    private City cityOfResidence;
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) {
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {}
    }
    DOMAIN MODEL

    View Slide

  27. © 2023 Thoughtworks | Confidential 27
    public class Student {
    private List enrolledCourses;
    private City cityOfResidence;
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS)
    {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) {
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {
    }
    }
    DOMAIN MODEL

    View Slide

  28. © 2023 Thoughtworks | Confidential 28
    public class Student {
    private List enrolledCourses;
    private City cityOfResidence;
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS
    || cityOfResidence.equals(ROME) && course.equals(ROMAN_HISTORY))
    {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) {
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {
    }
    }
    DOMAIN MODEL

    View Slide

  29. © 2023 Thoughtworks | Confidential 29
    public class Student {
    private List enrolledCourses;
    private City cityOfResidence;
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS
    || cityOfResidence.equals(ROME) && course.equals(ROMAN_HISTORY))
    {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) {
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {
    }
    }
    DOMAIN MODEL

    View Slide

  30. © 2023 Thoughtworks | Confidential 30
    public class Student {
    private List enrolledCourses;
    private City cityOfResidence;
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS
    || cityOfResidence.equals(ROME) && course.equals(ROMAN_HISTORY))
    {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() > MAX_COURSE_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {
    }
    }
    DOMAIN MODEL

    View Slide

  31. © 2023 Thoughtworks | Confidential 31
    public class Student {
    private List enrolledCourses;
    private City cityOfResidence;
    public void enrollInCourse(Course course) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() < MAX_COURSE_ENROLLMENTS
    || cityOfResidence.equals(ROME) && course.equals(ROMAN_HISTORY))
    {
    enrolledCourses.add(course);
    } else {
    throw new EnrollmentLimitExceeded();
    }
    }
    public void changeCityOfResidence(City newCityOfResidence) throws EnrollmentLimitExceeded {
    if (enrolledCourses.size() > MAX_COURSE_ENROLLMENTS) {
    throw new EnrollmentLimitExceeded();
    }
    this.cityOfResidence = newCityOfResidence;
    }
    private static class EnrollmentLimitExceeded extends DomainException {
    }
    }
    This is why aggregates are useful:
    ● a place where invariants are maintained
    ● all relevant information at hand
    ● the simplest code
    ● no distractions from FX

    View Slide

  32. © 2023 Thoughtworks | Confidential 32
    Transaction Script vs. Domain Model
    A transaction script is appropriate when
    ● business logic is simple,
    ● and changes infrequently
    In all other cases, a domain model is likely more appropriate

    View Slide

  33. © 2023 Thoughtworks | Confidential
    Trouble with aggregates
    33

    View Slide

  34. © 2023 Thoughtworks | Confidential 34
    Business rule #4
    “A course may have a maximum of 100 enrolled students”

    View Slide

  35. © 2023 Thoughtworks | Confidential 35
    Business rule #4
    “A course may have a maximum of 100 enrolled students”
    🤔
    Now enrollment
    may fail for
    another reason!
    Course Student
    Enrollment
    has many has many
    Which is the
    aggregate root here?
    The Course?
    Or the Student?
    Max 100
    enrollments
    Max 10
    enrollments
    Shouldn’t aggregates
    be small?

    View Slide

  36. © 2023 Thoughtworks | Confidential
    Domain service
    Sometimes, it just isn’t a thing. . . .
    When a significant process or transformation in
    the domain is not a natural responsibility of an
    ENTITY or VALUE OBJECT,
    add an operation to the model as a standalone
    interface declared as a SERVICE
    36

    View Slide

  37. © 2023 Thoughtworks | Confidential
    Domain service vs Application service
    37
    Watch out:
    a domain service
    is not an
    application service!
    Domain service:
    - stateless
    - no fx
    - yes domain logic
    Application service:
    - stateless
    - yes fx
    - no domain logic

    View Slide

  38. © 2023 Thoughtworks | Confidential
    All together
    38
    Enrollment
    Course
    Student
    Web controller
    Application
    service
    Queue listener
    Domain
    service
    Repository
    1
    2
    3
    4
    5
    Recreates from DB
    The domain service is now the entry
    point to the domain.
    In this case, it coordinates updates on
    two aggregates, ensuring all business
    rules are enforced
    FX
    NOFX

    View Slide

  39. © 2023 Thoughtworks | Confidential
    Just the facts
    39

    View Slide

  40. © 2023 Thoughtworks | Confidential
    Repository
    A Repository mediates between the domain and data
    mapping layers,
    acting like an in-memory domain object collection
    40
    https://martinfowler.com/eaaCatalog/repository.html

    View Slide

  41. © 2023 Thoughtworks | Confidential
    The repository “rehydrates” the domain model
    41
    Enrollment
    Student
    Repository
    Recreates from DB
    We delegate all persistence to the
    repository.
    But who is doing the coordination
    between the two?
    FX
    NOFX
    has many
    Invokes the aggregate root

    View Slide

  42. © 2023 Thoughtworks | Confidential
    “Service Layer”
    A Service Layer defines an application's boundary and its
    set of available operations from the perspective of
    interfacing client layers.
    It encapsulates the application's business logic,
    controlling transactions and coordinating responses in
    the implementation of its operations.
    42
    https://martinfowler.com/eaaCatalog/serviceLayer.html

    View Slide

  43. © 2023 Thoughtworks | Confidential
    Execute business operation
    The application service coordinates
    persistence and domain logic
    43
    Enrollment
    Student
    Application
    service
    Repository
    1
    2
    3
    Recreate from DB
    FX
    NOFX
    has many
    Find the aggregate 4
    Save the results

    View Slide

  44. © 2023 Thoughtworks | Confidential 44
    public class StudentEnrollmentApplicationService {
    private StudentRepository studentRepository;
    public StudentEnrollmentApplicationService(StudentRepository studentRepository) {
    this.studentRepository = studentRepository;
    }
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    Student student = studentRepository.findById(request.studentId());
    student.enrollInCourse(request.courseId());
    studentRepository.save(student);
    }
    }
    FX
    FX
    NOFX

    View Slide

  45. © 2023 Thoughtworks | Confidential 45
    public class StudentEnrollmentApplicationService {
    private StudentRepository studentRepository;
    public StudentEnrollmentApplicationService(StudentRepository studentRepository) {
    this.studentRepository = studentRepository;
    }
    public void enrollStudentInCourse(StudentCourseEnrollmentRequest request) {
    Student student = studentRepository.findById(request.studentId());
    student.enrollInCourse(request.courseId());
    studentRepository.save(student);
    }
    }
    FX
    FX
    NOFX
    Implementing “save” can
    be complicated
    “save” detects changes
    and persists them

    View Slide

  46. © 2023 Thoughtworks | Confidential
    Object-relational mapping to the rescue (?)
    ORMs make it “easy” to persist complex
    domain models.
    BUT!
    ● MAGIC
    ● Tight coupling of data and domain
    model
    46

    View Slide

  47. © 2023 Thoughtworks | Confidential
    Magic Is Bad
    Object-relational mapping to the rescue (?)
    ORMs make it “easy” to persist complex
    domain models.
    BUT!
    ● Tight coupling of data and domain
    model
    ● MAGIC
    47

    View Slide

  48. © 2023 Thoughtworks | Confidential
    Now we have two models
    48
    Student Enrollment
    has many
    Student Enrollment
    has many
    data model domain model

    View Slide

  49. © 2023 Thoughtworks | Confidential
    Now we have two models
    49
    Student Enrollment
    has many
    Student Enrollment
    has many
    data model domain model
    Spot the
    differences!

    View Slide

  50. © 2023 Thoughtworks | Confidential
    Now we have two models
    50
    Student Enrollment
    has many
    Student Enrollment
    has many
    data model domain model
    The 80’s style of persistence:
    1. The DB has the latest version of the state of the domain model
    2. The structure of the data and domain model are the same
    3. If you’re interested in modification history, it requires (a lot of) extra work
    4. If you’re using ORM, your DB is tightly coupled to your domain model
    5. If you don’t use ORM, it’s a lot of work to
    a. detect and save changes
    b. protect against concurrent changes

    View Slide

  51. © 2023 Thoughtworks | Confidential
    Enter Event Sourcing
    Greg Young introduced Event Sourcing circa 2005
    as a radically different way to implement business
    applications.
    However.
    ● It requires a specialized event store
    ● It is non-trivial to make it work in practice
    ● And this is why it’s best to use a framework
    51

    View Slide

  52. © 2023 Thoughtworks | Confidential
    “Just the facts”
    Taking inspiration from Event Sourcing
    ● Save in the DB the commands that were
    received…
    ○ … and the outcome (accepted or rejected)
    ● When a new command arrives:
    ○ you apply all the previous commands to the aggregate
    ○ the aggregate decides whether to accept or reject the
    command
    ○ save the command and the response we sent back
    ● It’s a bit like event sourcing, but
    ○ You save commands not (just) events
    ○ You don’t need a specialized data store, use a standard DB
    ○ You don’t need to “buy” the event sourcing architecture
    52

    View Slide

  53. © 2023 Thoughtworks | Confidential
    “Just the facts”
    The commands (and events) you receive are “facts”
    You don’t need anything else but the facts to
    reconstruct the truth of your system
    This style may not be applicable everywhere, but it
    works well where it can
    53

    View Slide

  54. © 2023 Thoughtworks | Confidential
    This pattern was used successfully
    1. A system to track tire lifetime through an embedded sensor
    2. A system to quote car insurance policies
    54

    View Slide

  55. © 2023 Thoughtworks | Confidential
    The two aspects of applications
    55
    1. Behaviours: what the application does
    2. Data: the information that must be persisted
    These two forces are in opposition
    The behaviours
    ● Are needed to make the application useful
    ● Change frequently with business changes
    The data
    ● Are needed to support the behaviours
    ● Are needed to support the business
    ● May outlive the application

    View Slide

  56. © 2023 Thoughtworks | Confidential
    Behaviours are implemented in application code → domain model
    Data are implemented in databases → data model
    The domain model and data model can be (much) different
    The two models serve different purposes
    56

    View Slide

  57. © 2023 Thoughtworks | Confidential
    “Just the facts”
    ● Save in the DB the commands that were received…
    ○ … and the outcome (accepted or rejected)
    ● When a new command arrives:
    ○ you apply all the previous commands to the aggregate
    ○ the aggregate decides whether to accept or reject the command
    ○ save the command and the response we sent back
    ● It’s a bit like event sourcing, but
    ○ You save commands not events
    ○ You don’t need a specialized data store, use a standard DB
    ○ You don’t need to “buy” the event sourcing architecture
    57

    View Slide

  58. © 2023 Thoughtworks | Confidential
    Where to learn more
    58

    View Slide

  59. © 2023 Thoughtworks | Confidential 59
    This book, published in 2002, was extremely influential.
    DHH, the author of Ruby on Rails, explicitly mentions it as
    an inspiration. And many, many frameworks that came after
    RoR were influenced by it.
    The book consists of 2 parts; the first part is narrative, and
    the second part is a catalog of patterns.
    https://martinfowler.com/books/eaa.html

    View Slide

  60. © 2023 Thoughtworks | Confidential 60
    Straight from DHH:
    The key tome to study for how to break down large problem spaces into
    beautiful domain models is Eric Evan's Domain-Driven Design. But you
    should only graduate to that level of strategic, architectural aspirations
    after you've mastered the basics of tactical programming through books
    like Kent Beck's Smalltalk Best Practices (if you work in object-orientated
    languages) and Martin Fowler's Patterns of Enterprise Application
    Architecture.

    View Slide

  61. © 2023 Thoughtworks | Confidential 61
    Thanks to Sara Pellegrini for her insightful blog series
    on Kill Aggregate that inspired this presentation!

    View Slide

  62. © 2023 Thoughtworks | Confidential
    Thank you!
    Matteo Vaccari
    [email protected]
    62

    View Slide