├── LICENSE ├── README.md └── listings ├── 02-01.feature ├── 05-01.cs ├── 05-02.cs ├── 05-03.cs ├── 05-04.cs ├── 05-05.cs ├── 05-06.cs ├── 05-07.cs ├── 05-08.cs ├── 05-09.cs ├── 06-01.cs ├── 06-02.cs ├── 06-03.cs ├── 06-04.cs ├── 06-05.cs ├── 06-06.cs ├── 06-07.cs ├── 06-08.cs ├── 06-09.cs ├── 06-10.cs ├── 06-11.cs ├── 06-12.sql ├── 06-13.cs ├── 06-14.cs ├── 06-15.cs ├── 06-16.json ├── 06-17.cs ├── 06-18.cs ├── 06-19.cs ├── 07-01.json ├── 07-02.cs ├── 07-03.cs ├── 07-04.cs ├── 07-05.cs ├── 07-06.cs ├── 07-07.cs ├── 08-01.cs ├── 08-02.cs ├── 08-03.cs ├── 08-04.cs ├── 09-01.cs ├── 09-02.cs ├── 09-03.json ├── 09-04.cs ├── 09-05.cs ├── 09-06.cs ├── 11-01.cs ├── 11-02.cs ├── 11-03.cs ├── 11-04.json ├── 11-05.json ├── 15-01.json ├── 15-02.json ├── 15-03.json ├── 15-04.json └── 15-05.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vladik Khononov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning Domain-Driven Design 2 | 3 | Thank you for reading the 🐒 book! 4 | 5 | The "listings" folder contains all the code samples used in the book. The naming format is the number of a chapter followed by the ordinal number of the code sample in the chapter. 6 | 7 | One more thing... keep an eye on this repository, as I'm working on some bonus content for the 🐒 book's readers: an open-source implementation of a subset of the Wolfdesk ticketing system described in the book's Preface. -------------------------------------------------------------------------------- /listings/02-01.feature: -------------------------------------------------------------------------------- 1 | Scenario: Notify agents about new support cases 2 | Given Vincent Jules submits a new support case saying: 3 | """ 4 | I need help configuring AWS Infinidash. 5 | """ 6 | When the ticket is assigned to Mr. Wolf 7 | Then Mr. Wolf receives a notification about the new support case -------------------------------------------------------------------------------- /listings/05-01.cs: -------------------------------------------------------------------------------- 1 | DB.StartTransaction(); 2 | 3 | var job = DB.LoadNextJob(); 4 | var json = LoadFile(job.Source); 5 | var xml = ConvertJsonToXml(json); 6 | WriteFile(job.Destination, xml.ToString(); 7 | DB.MarkJobAsCompleted(job); 8 | 9 | DB.Commit() -------------------------------------------------------------------------------- /listings/05-02.cs: -------------------------------------------------------------------------------- 1 | public class LogVisit 2 | { 3 | // ... 4 | 5 | public void Execute(Guid userId, DataTime visitedOn) 6 | { 7 | _db.Execute(“UPDATE Users SET last_visit=@p1 WHERE user_id=@p2”, 8 | visitedOn, userId); 9 | _db.Execute(@“INSERT INTO VisitsLog(user_id, visit_date) 10 | VALUES(@p1, @p2)”, userId, visitedOn); 11 | } 12 | } -------------------------------------------------------------------------------- /listings/05-03.cs: -------------------------------------------------------------------------------- 1 | public class LogVisit 2 | { 3 | // ... 4 | 5 | public void Execute(Guid userId, DataTime visitedOn) 6 | { 7 | try 8 | { 9 | _db.StartTransaction(); 10 | 11 | _db.Execute(@“UPDATE Users SET last_visit=@p1 12 | WHERE user_id=@p2”, 13 | visitedOn, userId); 14 | 15 | _db.Execute(@“INSERT INTO VisitsLog(user_id, visit_date) 16 | VALUES(@p1, @p2)”, 17 | userId, visitedOn); 18 | 19 | _db.Commit(); 20 | } catch { 21 | _db.Rollback(); 22 | throw; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /listings/05-04.cs: -------------------------------------------------------------------------------- 1 | public class LogVisit 2 | { 3 | // ... 4 | 5 | public void Execute(Guid userId, DataTime visitedOn) 6 | { 7 | _db.Execute(“UPDATE Users SET last_visit=@p1 WHERE user_id=@p2”, 8 | visitedOn,userId); 9 | _messageBus.Publish(“VISITS_TOPIC”, 10 | new { UserId = userId, VisitDate = visitedOn }); 11 | } 12 | } -------------------------------------------------------------------------------- /listings/05-05.cs: -------------------------------------------------------------------------------- 1 | public class LogVisit 2 | { 3 | // ... 4 | 5 | public void Execute(Guid userId) 6 | { 7 | _db.Execute(“UPDATE Users SET visits=visits+1 WHERE user_id=@p1”, 8 | userId); 9 | } 10 | } -------------------------------------------------------------------------------- /listings/05-06.cs: -------------------------------------------------------------------------------- 1 | public class LogVisit 2 | { 3 | // ... 4 | 5 | public void Execute(Guid userId, long visits) 6 | { 7 | _db.Execute(“UPDATE Users SET visits = @p1 WHERE user_id=@p2”, 8 | visits, userId); 9 | } 10 | } -------------------------------------------------------------------------------- /listings/05-07.cs: -------------------------------------------------------------------------------- 1 | public class LogVisit 2 | { 3 | // ... 4 | 5 | public void Execute(Guid userId, long expectedVisits) 6 | { 7 | _db.Execute(@“UPDATE Users SET visits=visits+1 8 | WHERE user_id=@p1 and visits = @p2”, 9 | userId, visits); 10 | } 11 | } -------------------------------------------------------------------------------- /listings/05-08.cs: -------------------------------------------------------------------------------- 1 | public class CreateUser 2 | { 3 | // ... 4 | 5 | public void Execute(userDetails) 6 | { 7 | try 8 | { 9 | _db.StartTransaction(); 10 | 11 | var user = new User(); 12 | user.Name = userDetails.Name; 13 | user.Email = userDetails.Email; 14 | user.Save(); 15 | 16 | _db.Commit(); 17 | } catch { 18 | _db.Rollback(); 19 | throw; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /listings/05-09.cs: -------------------------------------------------------------------------------- 1 | public void CreateTicket(TicketData data) 2 | { 3 | var agent = FindLeastBusyAgent(); 4 | agent.ActiveTickets = agent.ActiveTickets + 1; 5 | agent.Save(); 6 | var ticket = new Ticket(); 7 | ticket.Id = Guid.New(); 8 | ticket.Data = data; 9 | ticket.AssignedAgent = agent; 10 | ticket.Save(); 11 | _alerts.Send(agent, “You have a new ticket!”); 12 | } -------------------------------------------------------------------------------- /listings/06-01.cs: -------------------------------------------------------------------------------- 1 | class Color 2 | { 3 | int _red; 4 | int _green; 5 | int _blue; 6 | } -------------------------------------------------------------------------------- /listings/06-02.cs: -------------------------------------------------------------------------------- 1 | class Person 2 | { 3 | private int _id; 4 | private string _firstName; 5 | private string _lastName; 6 | private string _landlinePhone; 7 | private string _mobilePhone; 8 | private string _email; 9 | private int _heightMetric; 10 | private string _countryCode; 11 | 12 | public Person(...) {...} 13 | } 14 | 15 | static void Main(string[] args) 16 | { 17 | var dave = new Person( 18 | id: 30217, 19 | firstName: "Dave", 20 | lastName: "Ancelovici", 21 | landlinePhone: "023745001", 22 | mobilePhone: "0873712503", 23 | email: "dave@learning-ddd.com", 24 | heightMetric: 180, 25 | countryCode: "BG"); 26 | } -------------------------------------------------------------------------------- /listings/06-03.cs: -------------------------------------------------------------------------------- 1 | class Person 2 | { 3 | private PersonId _id; 4 | private Name _name; 5 | private PhoneNumber _landline; 6 | private PhoneNumber _mobile; 7 | private EmailAddress _email; 8 | private Height _height; 9 | private CountryCode _country; 10 | 11 | public Person(...) { ... } 12 | } 13 | 14 | static void Main(string[] args) 15 | { 16 | var dave = new Person( 17 | id: new PersonId(30217), 18 | name: new Name("Dave", "Ancelovici"), 19 | landline: PhoneNumber.Parse("023745001"), 20 | mobile: PhoneNumber.Parse("0873712503"), 21 | email: Email.Parse("dave@learning-ddd.com"), 22 | height: Height.FromMetric(180), 23 | country: CountryCode.Parse("BG")); 24 | } -------------------------------------------------------------------------------- /listings/06-04.cs: -------------------------------------------------------------------------------- 1 | var heightMetric = Height.Metric(180); 2 | var heightImperial = Height.Imperial(5, 3); 3 | 4 | var string1 = heightMetric.ToString(); // "180cm" 5 | var string2 = heightImperial.ToString(); // "5 feet 3 inches" 6 | var string3 = height1.ToImperial().ToString(); // "5 feet 11 inches" 7 | 8 | var firstIsHigher = heightMetric > heightImperial; // true 9 | 10 | var phone = PhoneNumber.Parse("+359877123503"); 11 | var country = phone.Country; // "BG" 12 | var phoneType = phone.PhoneType; // "MOBILE" 13 | var isValid = PhoneNumber.IsValid("+972120266680"); // false 14 | 15 | var red = Color.FromRGB(255, 0, 0); 16 | var green = Color.Green; 17 | var yellow = red.MixWith(green); 18 | var yellowString = yellow.ToString(); // "#FFFF00" -------------------------------------------------------------------------------- /listings/06-05.cs: -------------------------------------------------------------------------------- 1 | public class Color 2 | { 3 | public readonly byte Red; 4 | public readonly byte Green; 5 | public readonly byte Blue; 6 | 7 | public Color(byte r, byte g, byte b) 8 | { 9 | this.Red = r; 10 | this.Green = g; 11 | this.Blue = b; 12 | } 13 | 14 | public Color MixWith(Color other) 15 | { 16 | return new Color( 17 | r: (byte) Math.Min(this.Red + other.Red, 255), 18 | g: (byte) Math.Min(this.Green + other.Green, 255), 19 | b: (byte) Math.Min(this.Blue + other.Blue, 255) 20 | ); 21 | } 22 | 23 | public override bool Equals(object obj) 24 | { 25 | var other = obj as Color; 26 | return other != null && 27 | this.Red == other.Red && 28 | this.Green == other.Green && 29 | this.Blue == other.Blue; 30 | } 31 | 32 | public static bool operator == (Color lhs, Color rhs) 33 | { 34 | if (Object.ReferenceEquals(lhs, null)) { 35 | return Object.ReferenceEquals(rhs, null); 36 | } 37 | return lhs.Equals(rhs); 38 | } 39 | 40 | public static bool operator != (Color lhs, Color rhs) 41 | { 42 | return !(lhs == rhs); 43 | } 44 | 45 | public override int GetHashCode() 46 | { 47 | return ToString().GetHashCode(); 48 | } 49 | 50 | // ... 51 | } -------------------------------------------------------------------------------- /listings/06-06.cs: -------------------------------------------------------------------------------- 1 | class Person 2 | { 3 | public Name Name { get; set; } 4 | 5 | public Person(Name name) 6 | { 7 | this.Name = name; 8 | } 9 | } -------------------------------------------------------------------------------- /listings/06-07.cs: -------------------------------------------------------------------------------- 1 | class Person 2 | { 3 | public readonly PersonId Id; 4 | public Name Name { get; set; } 5 | 6 | public Person(PersonId id, Name name) 7 | { 8 | this.Id = id; 9 | this.Name = name; 10 | } 11 | } -------------------------------------------------------------------------------- /listings/06-08.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | // ... 4 | 5 | public void AddMessage(UserId from, string subject, string body) 6 | { 7 | var message = new Message(from, body); 8 | _messages.Append(message); 9 | } 10 | 11 | // ... 12 | } -------------------------------------------------------------------------------- /listings/06-09.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | // ... 4 | 5 | public void Execute(AddMessage cmd) 6 | { 7 | var message = new Message(cmd.from, cmd.body); 8 | _messages.Append(message); 9 | } 10 | 11 | // ... 12 | } -------------------------------------------------------------------------------- /listings/06-10.cs: -------------------------------------------------------------------------------- 1 | public ExecutionResult Escalate(TicketId id, EscalationReason reason) 2 | { 3 | try 4 | { 5 | var ticket = _ticketRepository.load(id); 6 | var cmd = new Escalate(); 7 | ticket.Execute(cmd); 8 | _ticketRepository.Save(ticket); 9 | return ExecutionResult.Success(); 10 | } 11 | catch (ConcurrencyException ex) 12 | { 13 | return ExecutionResult.Error(ex); 14 | } 15 | } -------------------------------------------------------------------------------- /listings/06-11.cs: -------------------------------------------------------------------------------- 1 | class Ticket 2 | { 3 | TicketId _id; 4 | int _version; 5 | 6 | // ... 7 | } 8 | -------------------------------------------------------------------------------- /listings/06-12.sql: -------------------------------------------------------------------------------- 1 | UPDATE tickets 2 | SET ticket_status = @new_status, 3 | agg_version = agg_version + 1, 4 | WHERE ticket_id=@id and agg_version=@expected_version; -------------------------------------------------------------------------------- /listings/06-13.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | // ... 4 | List _messages; 5 | // ... 6 | 7 | public void Execute(EvaluateAutomaticActions cmd) 8 | { 9 | if (this.IsEscalated && this.RemainingTimePercentage < 0.5 && 10 | GetUnreadMessagesCount(forAgent: AssignedAgent) > 0) 11 | { 12 | _agent = AssignNewAgent(); 13 | } 14 | } 15 | 16 | public int GetUnreadMessagesCount(UserId id) 17 | { 18 | return _messages.Where(x => x.To == id && !x.WasRead).Count(); 19 | } 20 | 21 | // ... 22 | } -------------------------------------------------------------------------------- /listings/06-14.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | private UserId _customer; 4 | private List _product; 5 | private UserId _assignedAgent; 6 | private List _messages; 7 | 8 | // ... 9 | } -------------------------------------------------------------------------------- /listings/06-15.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | // ... 4 | List _messages; 5 | // ... 6 | 7 | public void Execute(AcknowledgeMessage cmd) 8 | { 9 | var message = _messages.Where(x => x.Id == cmd.id).First(); 10 | message.WasRead = true; 11 | } 12 | // ... 13 | } -------------------------------------------------------------------------------- /listings/06-16.json: -------------------------------------------------------------------------------- 1 | { 2 | "ticket-id": "c9d286ff-3bca-4f57-94d4-4d4e490867d1", 3 | "event-id": 146, 4 | "event-type": "ticket-escalated", 5 | "escalation-reason": "missed-sla", 6 | "escalation-time": 1628970815 7 | } -------------------------------------------------------------------------------- /listings/06-17.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | // ... 4 | private List _domainEvents; 5 | // ... 6 | 7 | public void Execute(RequestEscalation cmd) 8 | { 9 | if (!this.IsEscalated && this.RemainingTimePercentage <= 0) 10 | { 11 | this.IsEscalated = true; 12 | var escalatedEvent = new TicketEscalated(_id); 13 | _domainEvents.Append(escalatedEvent); 14 | } 15 | } 16 | 17 | // ... 18 | } -------------------------------------------------------------------------------- /listings/06-18.cs: -------------------------------------------------------------------------------- 1 | public class ResponseTimeFrameCalculationService 2 | { 3 | // ... 4 | 5 | public ResponseTimeframe CalculateAgentResponseDeadline(UserId agentId, 6 | Priority priority, bool escalated, DateTime startTime) 7 | { 8 | var policy = _departmentRepository.GetDepartmentPolicy(agentId); 9 | var maxProcTime = policy.GetMaxResponseTimeFor(priority); 10 | 11 | if (escalated) { 12 | maxProcTime = maxProcTime * policy.EscalationFactor; 13 | } 14 | 15 | var shifts = _departmentRepository.GetUpcomingShifts(agentId, 16 | startTime, startTime.Add(policy.MaxAgentResponseTime)); 17 | 18 | return CalculateTargetTime(maxProcTime, shifts); 19 | } 20 | 21 | // ... 22 | } -------------------------------------------------------------------------------- /listings/06-19.cs: -------------------------------------------------------------------------------- 1 | public class ClassA 2 | { 3 | public int A { get; set; } 4 | public int B { get; set; } 5 | public int C { get; set; } 6 | public int D { get; set; } 7 | public int E { get; set; } 8 | } 9 | 10 | public class ClassB 11 | { 12 | private int _a, _d; 13 | 14 | public int A 15 | { 16 | get => _a; 17 | set 18 | { 19 | _a = value; 20 | B = value / 2; 21 | C = value / 3; 22 | } 23 | } 24 | 25 | public int B { get; private set; } 26 | 27 | public int C { get; private set; } 28 | 29 | public int D 30 | { 31 | get => _d; 32 | set 33 | { 34 | _d = value; 35 | E = value * 2 36 | } 37 | } 38 | 39 | public int E { get; private set; } 40 | } -------------------------------------------------------------------------------- /listings/07-01.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "lead-id": 12, 4 | "event-id": 0, 5 | "event-type": "lead-initialized", 6 | "first-name": "Casey", 7 | "last-name": "David", 8 | "phone-number": "555-2951", 9 | "timestamp": "2020-05-20T09:52:55.95Z" 10 | }, 11 | { 12 | "lead-id": 12, 13 | "event-id": 1, 14 | "event-type": "contacted", 15 | "timestamp": "2020-05-20T12:32:08.24Z" 16 | }, 17 | { 18 | "lead-id": 12, 19 | "event-id": 2, 20 | "event-type": "followup-set", 21 | "followup-on": "2020-05-27T12:00:00.00Z", 22 | "timestamp": "2020-05-20T12:32:08.24Z" 23 | }, 24 | { 25 | "lead-id": 12, 26 | "event-id": 3, 27 | "event-type": "contact-details-updated", 28 | "first-name": "Casey", 29 | "last-name": "Davis", 30 | "phone-number": "555-8101", 31 | "timestamp": "2020-05-20T12:32:08.24Z" 32 | }, 33 | { 34 | "lead-id": 12, 35 | "event-id": 4, 36 | "event-type": "contacted", 37 | "timestamp": "2020-05-27T12:02:12.51Z" 38 | }, 39 | { 40 | "lead-id": 12, 41 | "event-id": 5, 42 | "event-type": "order-submitted", 43 | "payment-deadline": "2020-05-30T12:02:12.51Z", 44 | "timestamp": "2020-05-27T12:02:12.51Z" 45 | }, 46 | { 47 | "lead-id": 12, 48 | "event-id": 6, 49 | "event-type": "payment-confirmed", 50 | "status": "converted", 51 | "timestamp": "2020-05-27T12:38:44.12Z" 52 | } 53 | ] -------------------------------------------------------------------------------- /listings/07-02.cs: -------------------------------------------------------------------------------- 1 | public class LeadSearchModelProjection 2 | { 3 | public long LeadId { get; private set; } 4 | public HashSet FirstNames { get; private set; } 5 | public HashSet LastNames { get; private set; } 6 | public HashSet PhoneNumbers { get; private set; } 7 | public int Version { get; private set; } 8 | 9 | public void Apply(LeadInitialized @event) 10 | { 11 | LeadId = @event.LeadId; 12 | FirstNames = new HashSet < string > (); 13 | LastNames = new HashSet < string > (); 14 | PhoneNumbers = new HashSet < PhoneNumber > (); 15 | FirstNames.Add(@event.FirstName); 16 | LastNames.Add(@event.LastName); 17 | PhoneNumbers.Add(@event.PhoneNumber); 18 | Version = 0; 19 | } 20 | 21 | public void Apply(ContactDetailsChanged @event) 22 | { 23 | FirstNames.Add(@event.FirstName); 24 | LastNames.Add(@event.LastName); 25 | PhoneNumbers.Add(@event.PhoneNumber); 26 | Version += 1; 27 | } 28 | 29 | public void Apply(Contacted @event) 30 | { 31 | Version += 1; 32 | } 33 | 34 | public void Apply(FollowupSet @event) 35 | { 36 | Version += 1; 37 | } 38 | 39 | public void Apply(OrderSubmitted @event) 40 | { 41 | Version += 1; 42 | } 43 | 44 | public void Apply(PaymentConfirmed @event) 45 | { 46 | Version += 1; 47 | } 48 | } -------------------------------------------------------------------------------- /listings/07-03.cs: -------------------------------------------------------------------------------- 1 | public class AnalysisModelProjection 2 | { 3 | public long LeadId { get; private set; } 4 | public int Followups { get; private set; } 5 | public LeadStatus Status { get; private set; } 6 | public int Version { get; private set; } 7 | 8 | public void Apply(LeadInitialized @event) 9 | { 10 | LeadId = @event.LeadId; 11 | Followups = 0; 12 | Status = LeadStatus.NEW_LEAD; 13 | Version = 0; 14 | } 15 | 16 | public void Apply(Contacted @event) 17 | { 18 | Version += 1; 19 | } 20 | 21 | public void Apply(FollowupSet @event) 22 | { 23 | Status = LeadStatus.FOLLOWUP_SET; 24 | Followups += 1; 25 | Version += 1; 26 | } 27 | 28 | public void Apply(ContactDetailsChanged @event) 29 | { 30 | Version += 1; 31 | } 32 | 33 | public void Apply(OrderSubmitted @event) 34 | { 35 | Status = LeadStatus.PENDING_PAYMENT; 36 | Version += 1; 37 | } 38 | 39 | public void Apply(PaymentConfirmed @event) 40 | { 41 | Status = LeadStatus.CONVERTED; 42 | Version += 1; 43 | } 44 | } -------------------------------------------------------------------------------- /listings/07-04.cs: -------------------------------------------------------------------------------- 1 | interface IEventStore 2 | { 3 | IEnumerable Fetch(Guid instanceId); 4 | void Append(Guid instanceId, Event[] newEvents, int expectedVersion); 5 | } -------------------------------------------------------------------------------- /listings/07-05.cs: -------------------------------------------------------------------------------- 1 | public class TicketAPI 2 | { 3 | private ITicketsRepository _ticketsRepository; 4 | // ... 5 | 6 | public void RequestEscalation(TicketId id) 7 | { 8 | var events = _ticketsRepository.LoadEvents(id); 9 | var ticket = new Ticket(events); 10 | var originalVersion = ticket.Version; 11 | var cmd = new RequestEscalation(); 12 | ticket.Execute(cmd); 13 | _ticketsRepository.CommitChanges(ticket, originalVersion); 14 | } 15 | 16 | // ... 17 | } -------------------------------------------------------------------------------- /listings/07-06.cs: -------------------------------------------------------------------------------- 1 | public class Ticket 2 | { 3 | // ... 4 | private List _domainEvents = new List(); 5 | private TicketState _state; 6 | // ... 7 | 8 | public Ticket(IEnumerable events) 9 | { 10 | _state = new TicketState(); 11 | foreach (var e in events) 12 | { 13 | AppendEvent(e); 14 | } 15 | } 16 | 17 | private void AppendEvent(IDomainEvent @event) 18 | { 19 | _domainEvents.Append(@event); 20 | // Dynamically call the correct overload of the “Apply” method. 21 | ((dynamic)state).Apply((dynamic)@event); 22 | } 23 | 24 | public void Execute(RequestEscalation cmd) 25 | { 26 | if (!_state.IsEscalated && _state.RemainingTimePercentage <= 0) 27 | { 28 | var escalatedEvent = new TicketEscalated(_id, cmd.Reason); 29 | AppendEvent(escalatedEvent); 30 | } 31 | } 32 | 33 | // ... 34 | } -------------------------------------------------------------------------------- /listings/07-07.cs: -------------------------------------------------------------------------------- 1 | public class TicketState 2 | { 3 | public TicketId Id { get; private set; } 4 | public int Version { get; private set; } 5 | public bool IsEscalated { get; private set; } 6 | // ... 7 | public void Apply(TicketInitialized @event) 8 | { 9 | Id = @event.Id; 10 | Version = 0; 11 | IsEscalated = false; 12 | // .... 13 | } 14 | 15 | public void Apply(TicketEscalated @event) 16 | { 17 | IsEscalated = true; 18 | Version += 1; 19 | } 20 | 21 | // ... 22 | } -------------------------------------------------------------------------------- /listings/08-01.cs: -------------------------------------------------------------------------------- 1 | namespace MvcApplication.Controllers 2 | { 3 | public class UserController: Controller 4 | { 5 | // ... 6 | 7 | [AcceptVerbs(HttpVerbs.Post)] 8 | public ActionResult Create(ContactDetails contactDetails) 9 | { 10 | OperationResult result = null; 11 | 12 | try 13 | { 14 | _db.StartTransaction(); 15 | 16 | var user = new User(); 17 | user.SetContactDetails(contactDetails) 18 | user.Save(); 19 | 20 | _db.Commit(); 21 | result = OperationResult.Success; 22 | } catch (Exception ex) { 23 | _db.Rollback(); 24 | result = OperationResult.Exception(ex); 25 | } 26 | 27 | return View(result); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /listings/08-02.cs: -------------------------------------------------------------------------------- 1 | interface CampaignManagementService 2 | { 3 | OperationResult CreateCampaign(CampaignDetails details); 4 | OperationResult Publish(CampaignId id, PublishingSchedule schedule); 5 | OperationResult Deactivate(CampaignId id); 6 | OperationResult AddDisplayLocation(CampaignId id, DisplayLocation newLocation); 7 | // ... 8 | } -------------------------------------------------------------------------------- /listings/08-03.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceLayer 2 | { 3 | public class UserService 4 | { 5 | // ... 6 | 7 | public OperationResult Create(ContactDetails contactDetails) 8 | { 9 | OperationResult result = null; 10 | 11 | try 12 | { 13 | _db.StartTransaction(); 14 | 15 | var user = new User(); 16 | user.SetContactDetails(contactDetails) 17 | user.Save(); 18 | 19 | _db.Commit(); 20 | result = OperationResult.Success; 21 | } catch (Exception ex) { 22 | _db.Rollback(); 23 | result = OperationResult.Exception(ex); 24 | } 25 | 26 | return result; 27 | } 28 | 29 | // ... 30 | } 31 | } 32 | 33 | namespace MvcApplication.Controllers 34 | { 35 | public class UserController: Controller 36 | { 37 | // ... 38 | 39 | [AcceptVerbs(HttpVerbs.Post)] 40 | public ActionResult Create(ContactDetails contactDetails) 41 | { 42 | var result = _userService.Create(contactDetails); 43 | return View(result); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /listings/08-04.cs: -------------------------------------------------------------------------------- 1 | namespace App.BusinessLogicLayer 2 | { 3 | public interface IMessaging 4 | { 5 | void Publish(Message payload); 6 | void Subscribe(Message type, Action callback); 7 | } 8 | } 9 | 10 | namespace App.Infrastructure.Adapters 11 | { 12 | public class SQSBus: IMessaging { /* ... */ } 13 | } -------------------------------------------------------------------------------- /listings/09-01.cs: -------------------------------------------------------------------------------- 1 | public class Campaign 2 | { 3 | // ... 4 | List _events; 5 | IMessageBus _messageBus; 6 | // ... 7 | 8 | public void Deactivate(string reason) 9 | { 10 | for (l in _locations.Values()) 11 | { 12 | l.Deactivate(); 13 | } 14 | 15 | IsActive = false; 16 | 17 | var newEvent = new CampaignDeactivated(_id, reason); 18 | _events.Append(newEvent); 19 | _messageBus.Publish(newEvent); 20 | } 21 | } -------------------------------------------------------------------------------- /listings/09-02.cs: -------------------------------------------------------------------------------- 1 | public class ManagementAPI 2 | { 3 | // ... 4 | private readonly IMessageBus _messageBus; 5 | private readonly ICampaignRepository _repository; 6 | // ... 7 | public ExecutionResult DeactivateCampaign(CampaignId id, string reason) 8 | { 9 | try 10 | { 11 | var campaign = repository.Load(id); 12 | campaign.Deactivate(reason); 13 | _repository.CommitChanges(campaign); 14 | 15 | var events = campaign.GetUnpublishedEvents(); 16 | for (IDomainEvent e in events) 17 | { 18 | _messageBus.publish(e); 19 | } 20 | campaign.ClearUnpublishedEvents(); 21 | } 22 | catch(Exception ex) 23 | { 24 | // ... 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /listings/09-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "campaign-id": "364b33c3-2171-446d-b652-8e5a7b2be1af", 3 | "state": { 4 | "name": "Autumn 2017", 5 | "publishing-state": "DEACTIVATED", 6 | "ad-locations": [ 7 | "..." 8 | ], 9 | "...": "..." 10 | }, 11 | "outbox": [ 12 | { 13 | "campaign-id": "364b33c3-2171-446d-b652-8e5a7b2be1af", 14 | "type": "campaign-deactivated", 15 | "reason": "Goals met", 16 | "published": false 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /listings/09-04.cs: -------------------------------------------------------------------------------- 1 | public class CampaignPublishingSaga 2 | { 3 | private readonly ICampaignRepository _repository; 4 | private readonly IPublishingServiceClient _publishingService; 5 | // ... 6 | 7 | public void Process(CampaignActivated @event) 8 | { 9 | var campaign = _repository.Load(@event.CampaignId); 10 | var advertisingMaterials = campaign.GenerateAdvertisingMaterials); 11 | _publishingService.SubmitAdvertisement(@event.CampaignId, 12 | advertisingMaterials); 13 | } 14 | 15 | public void Process(PublishingConfirmed @event) 16 | { 17 | var campaign = _repository.Load(@event.CampaignId); 18 | campaign.TrackPublishingConfirmation(@event.ConfirmationId); 19 | _repository.CommitChanges(campaign); 20 | } 21 | 22 | public void Process(PublishingRejected @event) 23 | { 24 | var campaign = _repository.Load(@event.CampaignId); 25 | campaign.TrackPublishingRejection(@event.RejectionReason); 26 | _repository.CommitChanges(campaign); 27 | } 28 | } -------------------------------------------------------------------------------- /listings/09-05.cs: -------------------------------------------------------------------------------- 1 | public class CampaignPublishingSaga 2 | { 3 | private readonly ICampaignRepository _repository; 4 | private readonly IList _events; 5 | // ... 6 | 7 | public void Process(CampaignActivated activated) 8 | { 9 | var campaign = _repository.Load(activated.CampaignId); 10 | var advertisingMaterials = campaign.GenerateAdvertisingMaterials); 11 | var commandIssuedEvent = new CommandIssuedEvent( 12 | target: Target.PublishingService, 13 | command: new SubmitAdvertisementCommand(activated.CampaignId, 14 | advertisingMaterials)); 15 | 16 | _events.Append(activated); 17 | _events.Append(commandIssuedEvent); 18 | } 19 | 20 | public void Process(PublishingConfirmed confirmed) 21 | { 22 | var commandIssuedEvent = new CommandIssuedEvent( 23 | target: Target.CampaignAggregate, 24 | command: new TrackConfirmation(confirmed.CampaignId, 25 | confirmed.ConfirmationId)); 26 | 27 | _events.Append(confirmed); 28 | _events.Append(commandIssuedEvent); 29 | } 30 | 31 | public void Process(PublishingRejected rejected) 32 | { 33 | var commandIssuedEvent = new CommandIssuedEvent( 34 | target: Target.CampaignAggregate, 35 | command: new TrackRejection(rejected.CampaignId, 36 | rejected.RejectionReason)); 37 | 38 | _events.Append(rejected); 39 | _events.Append(commandIssuedEvent); 40 | } 41 | } -------------------------------------------------------------------------------- /listings/09-06.cs: -------------------------------------------------------------------------------- 1 | public class BookingProcessManager 2 | { 3 | private readonly IList _events; 4 | private BookingId _id; 5 | private Destination _destination; 6 | private TripDefinition _parameters; 7 | private EmployeeId _traveler; 8 | private Route _route; 9 | private IList _rejectedRoutes; 10 | private IRoutingService _routing; 11 | // ... 12 | 13 | public void Initialize(Destination destination, 14 | TripDefinition parameters, 15 | EmployeeId traveler) 16 | { 17 | _destination = destination; 18 | _parameters = parameters; 19 | _traveler = traveler; 20 | _route = _routing.Calculate(destination, parameters); 21 | 22 | var routeGenerated = new RouteGeneratedEvent( 23 | BookingId: _id, 24 | Route: _route); 25 | 26 | var commandIssuedEvent = new CommandIssuedEvent( 27 | command: new RequestEmployeeApproval(_traveler, _route) 28 | ); 29 | 30 | _events.Append(routeGenerated); 31 | _events.Append(commandIssuedEvent); 32 | } 33 | 34 | public void Process(RouteConfirmed confirmed) 35 | { 36 | var commandIssuedEvent = new CommandIssuedEvent( 37 | command: new BookFlights(_route, _parameters) 38 | ); 39 | 40 | _events.Append(confirmed); 41 | _events.Append(commandIssuedEvent); 42 | } 43 | 44 | public void Process(RouteRejected rejected) 45 | { 46 | var commandIssuedEvent = new CommandIssuedEvent( 47 | command: new RequestRerouting(_traveler, _route) 48 | ); 49 | 50 | _events.Append(rejected); 51 | _events.Append(commandIssuedEvent); 52 | } 53 | 54 | public void Process(ReroutingConfirmed confirmed) 55 | { 56 | _rejectedRoutes.Append(route); 57 | _route = _routing.CalculateAltRoute(destination, 58 | parameters, rejectedRoutes); 59 | var routeGenerated = new RouteGeneratedEvent( 60 | BookingId: _id, 61 | Route: _route); 62 | 63 | var commandIssuedEvent = new CommandIssuedEvent( 64 | command: new RequestEmployeeApproval(_traveler, _route) 65 | ); 66 | 67 | _events.Append(confirmed); 68 | _events.Append(routeGenerated); 69 | _events.Append(commandIssuedEvent); 70 | } 71 | 72 | public void Process(FlightBooked booked) 73 | { 74 | var commandIssuedEvent = new CommandIssuedEvent( 75 | command: new BookHotel(_destination, _parameters) 76 | ); 77 | 78 | _events.Append(booked); 79 | _events.Append(commandIssuedEvent); 80 | } 81 | 82 | // ... 83 | } -------------------------------------------------------------------------------- /listings/11-01.cs: -------------------------------------------------------------------------------- 1 | public class Player 2 | { 3 | public Guid Id { get; set; } 4 | public int Points { get; set; } 5 | } 6 | 7 | public class ApplyBonus 8 | { 9 | // ... 10 | 11 | public void Execute(Guid playerId, byte percentage) 12 | { 13 | var player = _repository.Load(playerId); 14 | player.Points *= 1 + percentage/100.0; 15 | _repository.Save(player); 16 | } 17 | } -------------------------------------------------------------------------------- /listings/11-02.cs: -------------------------------------------------------------------------------- 1 | public class Player 2 | { 3 | public Guid Id { get; private set; } 4 | public int Points { get; private set; } 5 | } 6 | 7 | public class ApplyBonus 8 | { 9 | // ... 10 | 11 | public void Execute(Guid playerId, byte percentage) 12 | { 13 | var player = _repository.Load(playerId); 14 | player.Points *= 1 + percentage/100.0; 15 | _repository.Save(player); 16 | } 17 | } -------------------------------------------------------------------------------- /listings/11-03.cs: -------------------------------------------------------------------------------- 1 | public class Player 2 | { 3 | public Guid Id { get; private set; } 4 | public int Points { get; private set; } 5 | 6 | public void ApplyBonus(int percentage) 7 | { 8 | this.Points *= 1 + percentage/100.0; 9 | } 10 | } 11 | 12 | public class ApplyBonus 13 | { 14 | // ... 15 | 16 | public void Execute(Guid playerId, int percentage) 17 | { 18 | var player = _repository.Load(playerId); 19 | player.ApplyBonus(percentage); 20 | _repository.Save(player); 21 | } 22 | } -------------------------------------------------------------------------------- /listings/11-04.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "lead-id": 12, 4 | "event-id": 0, 5 | "event-type": "lead-initialized", 6 | "first-name": "Shauna", 7 | "last-name": "Mercia", 8 | "phone-number": "555-4753" 9 | }, 10 | { 11 | "lead-id": 12, 12 | "event-id": 1, 13 | "event-type": "contacted", 14 | "timestamp": "2020-05-27T12:02:12.51Z" 15 | }, 16 | { 17 | "lead-id": 12, 18 | "event-id": 2, 19 | "event-type": "order-submitted", 20 | "payment-deadline": "2020-05-30T12:02:12.51Z", 21 | "timestamp": "2020-05-27T12:02:12.51Z" 22 | }, 23 | { 24 | "lead-id": 12, 25 | "event-id": 3, 26 | "event-type": "payment-confirmed", 27 | "status": "converted", 28 | "timestamp": "2020-05-27T12:38:44.12Z" 29 | } 30 | ] -------------------------------------------------------------------------------- /listings/11-05.json: -------------------------------------------------------------------------------- 1 | { 2 | "lead-id": 12, 3 | "event-id": 0, 4 | "event-type": "migrated-from-legacy", 5 | "first-name": "Shauna", 6 | "last-name": "Mercia", 7 | "phone-number": "555-4753", 8 | "status": "converted", 9 | "last-contacted-on": "2020-05-27T12:02:12.51Z", 10 | "order-placed-on": "2020-05-27T12:02:12.51Z", 11 | "converted-on": "2020-05-27T12:38:44.12Z", 12 | "followup-on": null 13 | } -------------------------------------------------------------------------------- /listings/15-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "delivery-confirmed", 3 | "event-id": "14101928-4d79-4da6-9486-dbc4837bc612", 4 | "correlation-id": "08011958-6066-4815-8dbe-dee6d9e5ebac", 5 | "delivery-id": "05011927-a328-4860-a106-737b2929db4e", 6 | "timestamp": 1615718833, 7 | "payload": { 8 | "confirmed-by": "17bc9223-bdd6-4382-954d-f1410fd286bd", 9 | "delivery-time": 1615701406 10 | } 11 | } -------------------------------------------------------------------------------- /listings/15-02.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "paycheck-generated", 3 | "event-id": "537ec7c2-d1a1-2005-8654-96aee1116b72", 4 | "delivery-id": "05011927-a328-4860-a106-737b2929db4e", 5 | "timestamp": 1615726445, 6 | "payload": { 7 | "employee-id": "456123", 8 | "link": "/paychecks/456123/2021/01" 9 | } 10 | } -------------------------------------------------------------------------------- /listings/15-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "customer-updated", 3 | "event-id": "6b7ce6c6-8587-4e4f-924a-cec028000ce6", 4 | "customer-id": "01b18d56-b79a-4873-ac99-3d9f767dbe61", 5 | "timestamp": 1615728520, 6 | "payload": { 7 | "first-name": "Carolyn", 8 | "last-name": "Hayes", 9 | "phone": "555-1022", 10 | "status": "follow-up-set", 11 | "follow-up-date": "2021/05/08", 12 | "birthday": "1982/04/05", 13 | "version": 7 14 | } 15 | } -------------------------------------------------------------------------------- /listings/15-04.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "customer-updated", 3 | "event-id": "6b7ce6c6-8587-4e4f-924a-cec028000ce6", 4 | "customer-id": "01b18d56-b79a-4873-ac99-3d9f767dbe61", 5 | "timestamp": 1615728520, 6 | "payload": { 7 | "status": "follow-up-set", 8 | "follow-up-date": "2021/05/10", 9 | "version": 8 10 | } 11 | } -------------------------------------------------------------------------------- /listings/15-05.js: -------------------------------------------------------------------------------- 1 | eventNotification = { 2 | "type": "marriage-recorded", 3 | "person-id": "01b9a761", 4 | "payload": { 5 | "person-id": "126a7b61", 6 | "details": "/01b9a761/marriage-data" 7 | } 8 | }; 9 | 10 | ecst = { 11 | "type": "personal-details-changed", 12 | "person-id": "01b9a761", 13 | "payload": { 14 | "new-last-name": "Williams" 15 | } 16 | }; 17 | 18 | domainEvent = { 19 | "type": "married", 20 | "person-id": "01b9a761", 21 | "payload": { 22 | "person-id": "126a7b61", 23 | "assumed-partner-last-name": true 24 | } 25 | }; --------------------------------------------------------------------------------