بلوکهای سازنده
اکنون بلوکهای اصلی Domain Model یا الگوهای تاکتیکی DDD را بررسی میکنیم: Value Objectها، Aggregateها و Domain Serviceها.
شیء مقدار (Value Object)
Value Object شیئی است که با ترکیب مقادیرش شناسایی میشود. برای مثال شیء رنگ زیر را در نظر بگیرید:
class Color
{
int _red;
int _green;
int _blue;
}
ترکیب سه مقدار red، green و blue یک رنگ را تعریف میکند. تغییر هرکدام، رنگ جدیدی میسازد. دو رنگ با مقادیر یکسان در واقع همان رنگاند؛ بنابراین برای شناسایی رنگ نیازی به فیلد شناسهٔ صریح نیست.
فیلد ColorId در شکل ۶-۱ نهتنها زائد است بلکه میتواند منبع Bug باشد: ممکن است دو ردیف با RGB یکسان اما ColorId متفاوت داشته باشیم و مقایسهٔ شناسهها نتواند نشان دهد که هر دو یک رنگ را بازنمایی میکنند.
شکل ۶-۱ — فیلد زائد ColorId که امکان وجود دو ردیف با مقادیر یکسان را فراهم میکند.
زبان فراگیر
تکیهٔ صرف بر انواع ابتدایی کتابخانهٔ زبان، مانند String، Integer یا Dictionary، برای نمایش مفاهیم دامنه «Primitive Obsession» نامیده میشود. کلاس زیر را در نظر بگیرید:
class Person
{
private int _id;
private string _firstName;
private string _lastName;
private string _landlinePhone;
private string _mobilePhone;
private string _email;
private int _heightMetric;
private string _countryCode;
public Person(...) {...}
}
static void Main(string[] args)
{
var dave = new Person(
id: 30217,
firstName: "Dave",
lastName: "Ancelovici",
landlinePhone: "023745001",
mobilePhone: "0873712503",
email: "dave@learning-ddd.com",
heightMetric: 180,
countryCode: "BG");
}
در این پیادهسازی بیشتر مقادیر از نوع String هستند و معنای آنها بر اساس قرارداد ذهنی تعیین میشود. مثلاً landlinePhone باید شمارهٔ تلفن ثابت معتبر باشد و countryCode باید کد دوحرفی بزرگشدهٔ کشور باشد. سامانه نمیتواند همیشه به ورودی کاربر اعتماد کند، بنابراین کلاس مجبور است تمام ورودیها را اعتبارسنجی کند.
این رویکرد چند ریسک طراحی دارد: منطق Validation تکرار میشود؛ تضمین اینکه پیش از استفاده حتماً Validation فراخوانی شده دشوار است؛ و با بزرگشدن Codebase و ورود مهندسان دیگر، کنترل آن دشوارتر میشود.
حالا طراحی همان شیء با Value Objectها را مقایسه کنید:
class Person {
private PersonId _id;
private Name _name;
private PhoneNumber _landline;
private PhoneNumber _mobile;
private EmailAddress _email;
private Height _height;
private CountryCode _country;
public Person(...) { ... }
}
static void Main(string[] args)
{
var dave = new Person(
id: new PersonId(30217),
name: new Name("Dave", "Ancelovici"),
landline: PhoneNumber.Parse("023745001"),
mobile: PhoneNumber.Parse("0873712503"),
email: Email.Parse("dave@learning-ddd.com"),
height: Height.FromMetric(180),
country: CountryCode.Parse("BG"));
}
نخست، وضوح افزایش یافته است. مثلاً برای متغیر country لازم نیست نام طولانی countryCode استفاده شود تا معلوم شود کد کشور است نه نام کامل کشور؛ خود نوع Value Object قصد را روشن میکند.
دوم، پیش از Assignment نیازی به Validation جداگانه نیست، چون منطق اعتبارسنجی در خود Value Object قرار دارد. اما رفتار Value Object فقط Validation نیست. ارزش واقعی آن زمانی آشکار میشود که منطق کسبوکاری مربوط به دستکاری مقدار را متمرکز کند. منطق منسجم در یک مکان قرار میگیرد و بهسادگی Test میشود. مهمتر از همه، Value Object مفاهیم دامنه را بیان میکند و کد زبان فراگیر را به کار میگیرد.
مثلاً Value Object مربوط به قد، علاوه بر روشنکردن قصد، اندازهگیری را از واحد خاص جدا میکند و میتواند با واحد متریک یا امپریال مقداردهی شود، تبدیل انجام دهد، نمایش متنی بسازد و مقادیر را مقایسه کند:
var heightMetric = Height.Metric(180);
var heightImperial = Height.Imperial(5, 3);
var string1 = heightMetric.ToString(); // "180cm"
var string2 = heightImperial.ToString(); // "5 feet 3 inches"
var string3 = heightMetric.ToImperial().ToString(); // "5 feet 11 inches"
var firstIsHigher = heightMetric > heightImperial; // true
Value Object مربوط به PhoneNumber میتواند Parse و Validation و استخراج کشور و نوع شماره را کپسوله کند:
var phone = PhoneNumber.Parse("+359877123503");
var country = phone.Country; // "BG"
var phoneType = phone.PhoneType; // "MOBILE"
var isValid = PhoneNumber.IsValid("+972120266680"); // false
نمونهٔ زیر قدرت Value Object را وقتی همهٔ منطق دستکاری داده و تولید نمونههای جدید را در خود نگه میدارد نشان میدهد:
var red = Color.FromRGB(255, 0, 0);
var green = Color.Green;
var yellow = red.MixWith(green);
var yellowString = yellow.ToString(); // "#FFFF00"
Value Objectها نیاز به قراردادهای ذهنی مانند «این String ایمیل است و آن یکی شماره تلفن» را حذف و Object Model را شهودیتر و کمخطاتر میکنند.
پیادهسازی Value Object
از آنجا که تغییر هر فیلد یک Value Object از نظر مفهومی مقدار جدیدی ایجاد میکند، Value Objectها بهصورت Immutable پیادهسازی میشوند. عملیاتی مثل MixWith شیء اصلی را تغییر نمیدهد، بلکه نمونهٔ جدیدی بازمیگرداند:
public class Color
{
public readonly byte Red;
public readonly byte Green;
public readonly byte Blue;
public Color(byte r, byte g, byte b)
{
this.Red = r;
this.Green = g;
this.Blue = b;
}
public Color MixWith(Color other)
{
return new Color(
r: (byte) Math.Min(this.Red + other.Red, 255),
g: (byte) Math.Min(this.Green + other.Green, 255),
b: (byte) Math.Min(this.Blue + other.Blue, 255)
);
}
...
}
چون برابری Value Object بر اساس مقادیر است نه ID یا Reference، باید Equality بهدرستی پیادهسازی شود. برای مثال در C#:
public class Color
{
...
public override bool Equals(object obj)
{
var other = obj as Color;
return other != null &&
this.Red == other.Red &&
this.Green == other.Green &&
this.Blue == other.Blue;
}
public static bool operator == (Color lhs, Color rhs)
{
if (Object.ReferenceEquals(lhs, null)) {
return Object.ReferenceEquals(rhs, null);
}
return lhs.Equals(rhs);
}
public static bool operator != (Color lhs, Color rhs)
{
return !(lhs == rhs);
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
...
}
جالب است که با وجود اینکه استفاده از String عمومی برای ارزشهای دامنهای با ایدهٔ Value Object تضاد دارد، String در .NET و Java و زبانهای دیگر دقیقاً مانند Value Object پیادهسازی شده است: Immutable است و عملیات آن نمونهٔ جدید میسازد؛ همچنین رفتار غنی برای Trim، Concatenate، Replace، Substring و غیره دارد.
چه زمانی از Value Object استفاده کنیم؟
پاسخ ساده: هر زمان که میتوانید. Value Objectها علاوه بر بیانگرتر کردن کد و متمرکزکردن منطق کسبوکاری پراکنده، کد را امنتر میکنند. بهدلیل Immutable بودن، رفتارشان Side Effect ندارد و Thread-safe است.
از دید دامنه، قاعدهٔ خوبی این است که برای عناصر دامنه که ویژگی اشیای دیگر را توصیف میکنند Value Object بسازید. در مثال Person، شناسه، نام، شمارهها، ایمیل و قد همگی Value Object هستند. وضعیتها، Passwordها و مفاهیم دامنهای دیگری که با مقدارشان شناسایی میشوند نیز کاندیدا هستند. یکی از مهمترین موارد، پول و مقادیر مالی است. استفاده از Primitiveها برای پول هم کپسولهسازی منطق مالی را محدود میکند و هم میتواند خطاهای خطرناکی مانند Rounding و مشکلات Precision ایجاد کند.
موجودیتها (Entities)
Entity نقطهٔ مقابل Value Object است: برای تمایز نمونههای مختلف به یک فیلد شناسهٔ صریح نیاز دارد. Person نمونهٔ سادهای است. کلاس زیر فقط نام دارد:
class Person
{
public Name Name { get; set; }
public Person(Name name)
{
this.Name = name;
}
}
این طراحی ناقص است، چون افراد مختلف میتوانند دقیقاً همنام باشند ولی همان شخص نیستند. بنابراین باید شناسه اضافه شود:
class Person
{
public readonly PersonId Id;
public Name Name { get; set; }
public Person(PersonId id, Name name)
{
this.Id = id;
this.Name = name;
}
}
فیلد Id از نوع PersonId خود یک Value Object است و میتواند بر اساس نیاز دامنه، GUID، عدد، String یا حتی مقدار دامنهای مثل Social Security Number باشد.
الزام اصلی شناسه، یکتا بودن آن برای هر نمونهٔ Entity است؛ شکل ۶-۲. بهجز استثناهای نادر، مقدار شناسه باید در طول چرخهٔ عمر Entity تغییرناپذیر بماند. این موضوع ما را به تفاوت مفهومی دیگر میرساند: برخلاف Value Objectها، Entityها Immutable نیستند و انتظار میرود در طول زمان تغییر کنند. همچنین Value Objectها ویژگیهای Entity را توصیف میکنند.
شکل ۶-۲ — شناسهٔ صریح اجازه میدهد نمونهها حتی با مقادیر یکسان سایر فیلدها از هم متمایز شوند.
Entityها بلوک بنیادی هر دامنهٔ کسبوکارند، اما نویسنده آنها را مستقل در فهرست بلوکهای Domain Model نیاورد، چون Entityها مستقل پیادهسازی نمیشوند؛ فقط در زمینهٔ الگوی Aggregate استفاده میشوند.
تجمیعها (Aggregates)
Aggregate یک Entity است: شناسهٔ صریح دارد و State آن در چرخهٔ عمر تغییر میکند. اما بسیار فراتر از Entity ساده است. هدف الگو، حفاظت از سازگاری داده است. چون دادهٔ Aggregate Mutable است، الگو باید چالشهای ناشی از تغییر را حل کند تا State همیشه معتبر باقی بماند.
اجرای سازگاری
قابلتغییر بودن State راههای زیادی برای فساد داده ایجاد میکند. Aggregate مرز روشنی میان خود و محیط بیرونی میکشد و به یک مرز تضمین سازگاری تبدیل میشود. منطق Aggregate باید تمام تغییرات ورودی را اعتبارسنجی کند و مطمئن شود هیچ تغییری با قواعد کسبوکار تناقض ندارد.
از دید پیادهسازی، فقط منطق کسبوکار خود Aggregate اجازهٔ تغییر State را دارد. اشیا و فرایندهای بیرونی فقط میتوانند State را بخوانند؛ تغییر State باید از طریق متدهای رابط عمومی Aggregate انجام شود.
متدهای تغییردهندهٔ State که در رابط عمومی Aggregate قرار میگیرند معمولاً Command نامیده میشوند. Command میتواند یک متد عمومی عادی باشد:
public class Ticket
{
...
public void AddMessage(UserId from, string body)
{
var message = new Message(from, body);
_messages.Append(message);
}
...
}
یا به شکل Parameter Object که تمام ورودی لازم را کپسوله میکند:
public class Ticket
{
...
public void Execute(AddMessage cmd)
{
var message = new Message(cmd.from, cmd.body);
_messages.Append(message);
}
...
}
نحوهٔ بیان Command سلیقهای است؛ نویسنده روش صریحتر با ساختارهای Command و ارسال Polymorphic آنها به Execute را ترجیح میدهد.
رابط عمومی Aggregate مسئول Validation ورودی و اجرای تمام قواعد و Invariantهای مرتبط است. این مرز سخت تضمین میکند که تمام منطق مربوط به Aggregate در یک مکان، یعنی خود Aggregate، باقی بماند.
بنابراین Application Layer که عملیات Aggregate را هماهنگ میکند ساده است: State فعلی را Load میکند، Action را اجرا میکند، State تغییرکرده را ذخیره و نتیجه را بازمیگرداند:
01 public ExecutionResult Escalate(TicketId id, EscalationReason reason)
02 {
03 try
04 {
05 var ticket = _ticketRepository.Load(id);
06 var cmd = new Escalate(reason);
07 ticket.Execute(cmd);
08 _ticketRepository.Save(ticket);
09 return ExecutionResult.Success();
10 }
11 catch (ConcurrencyException ex)
12 {
13 return ExecutionResult.Error(ex);
14 }
15 }
کنترل Concurrency در خط ۱۱ حیاتی است. اگر چند Process همزمان همان Aggregate را Update کنند، نباید تراکنش دوم کورکورانه تغییر Commitشدهٔ اول را بازنویسی کند. Process دوم باید بفهمد State مبنای تصمیمش قدیمی شده و عملیات را Retry کند.
پس ذخیرهساز Aggregate باید مدیریت همزمانی را پشتیبانی کند. در سادهترین حالت Aggregate فیلد Version دارد که پس از هر Update افزایش مییابد:
class Ticket
{
TicketId _id;
int _version;
...
}
هنگام Commit باید مطمئن شویم Version بازنویسیشونده همان مقداری است که ابتدا خواندهایم. نمونهٔ SQL:
01 UPDATE tickets
02 SET ticket_status = @new_status,
03 agg_version = agg_version + 1
04 WHERE ticket_id=@id and agg_version=@expected_version;
این دستور State را تغییر میدهد و Version را افزایش میدهد، اما فقط وقتی Version فعلی همان مقدار مورد انتظار باشد. مدیریت Concurrency میتواند خارج از پایگاه دادهٔ رابطهای نیز انجام شود و Document Databaseها اغلب برای Aggregateها مناسباند؛ نکتهٔ مهم آن است که ذخیرهساز از Concurrency Management پشتیبانی کند.
مرز تراکنش
چون State Aggregate فقط توسط منطق خودش تغییر میکند، Aggregate همچنین یک مرز تراکنش است. همهٔ تغییرات State باید در یک عملیات اتمیک Commit شوند: یا همه یا هیچ.
هیچ عملیات سیستم نباید به تراکنشی متکی باشد که چند Aggregate را همزمان Commit کند. تغییر State هر Aggregate باید جداگانه، یک Aggregate در هر تراکنش پایگاه داده، Commit شود.
قانون «یک نمونهٔ Aggregate در هر تراکنش» ما را مجبور میکند مرز Aggregate را دقیق طراحی کنیم تا Invariantها و قواعد دامنه رعایت شوند. نیاز مستمر به Commit همزمان چند Aggregate علامت مرز تراکنشی و در نتیجه مرز Aggregate نادرست است.
سلسلهمراتب Entityها
سناریوهایی وجود دارد که چند شیء باید یک مرز تراکنشی مشترک داشته باشند؛ مثلاً وقتی همزمان تغییر میکنند یا قاعدهٔ یکی به State دیگری وابسته است. DDD میگوید طراحی باید توسط دامنه هدایت شود. Aggregate برای چنین حالتی یک سلسلهمراتب از Entityها میسازد که همه سازگاری تراکنشی مشترک دارند؛ شکل ۶-۳.
شکل ۶-۳ — Aggregate بهعنوان سلسلهمراتب Entityها.
این سلسلهمراتب هم Entity و هم Value Object دارد و همه زمانی در یک Aggregate قرار میگیرند که منطق کسبوکار دامنه آنها را به یک مرز تراکنش واحد پیوند دهد. نام Aggregate نیز از همین «تجمیع» موجودیتها و Value Objectهای یک مرز تراکنش میآید.
کد زیر قاعدهای را نشان میدهد که چند Entity در Aggregate را درگیر میکند: «اگر Agent تا زمانی که ۵۰٪ مهلت پاسخ باقی مانده یک تیکت Escalateشده را باز نکرده باشد، تیکت خودکار به Agent دیگری تخصیص داده شود»:
01 public class Ticket
02 {
03 ...
04 List<Message> _messages;
05 ...
06
07 public void Execute(EvaluateAutomaticActions cmd)
08 {
09 if (this.IsEscalated && this.RemainingTimePercentage < 0.5 &&
10 GetUnreadMessagesCount(for: 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 }
متد بررسی میکند تیکت Escalate شده باشد، زمان باقیمانده کمتر از ۵۰٪ باشد و پیام خواندهنشده برای Agent فعلی وجود داشته باشد. اگر همهٔ شرایط برقرار باشند، تیکت به Agent دیگری واگذار میشود. Aggregate تضمین میکند این شروط روی دادهٔ strongly consistent بررسی شوند و پس از بررسی، داده در میانهٔ عملیات تغییر نکند؛ چون همهٔ تغییرات Aggregate در یک تراکنش اتمیک انجام میشوند.
ارجاع به Aggregateهای دیگر
چون تمام اشیای درون Aggregate یک مرز تراکنشی مشترک دارند، بزرگشدن بیش از حد Aggregate مشکلات Performance و Scalability ایجاد میکند.
سازگاری داده معیار خوبی برای تعیین مرز Aggregate است. فقط اطلاعاتی که منطق کسبوکار برای آنها Strong Consistency لازم دارد باید داخل Aggregate باشند. اطلاعاتی که میتوانند Eventually Consistent باشند باید بیرون از مرز Aggregate، مثلاً در Aggregate دیگری، قرار گیرند؛ شکل ۶-۴.
شکل ۶-۴ — Aggregate بهعنوان مرز سازگاری.
قاعدهٔ عملی: Aggregateها را تا حد ممکن کوچک نگه دارید و فقط اشیایی را داخل آنها قرار دهید که منطق کسبوکار واقعاً به State کاملاً سازگارشان نیاز دارد:
public class Ticket
{
private UserId _customer;
private List<ProductId> _products;
private UserId _assignedAgent;
private List<Message> _messages;
...
}
در این مثال، پیامها داخل مرز Ticket Aggregate هستند. در مقابل، Customer، Products و Assigned Agent بیروناند و فقط با ID ارجاع داده میشوند. ارجاع به Aggregate خارجی از طریق ID صریح میکند که آن شیء عضو مرز Aggregate فعلی نیست و هر Aggregate مرز تراکنش خودش را دارد.
برای تصمیمگیری دربارهٔ عضویت یک Entity در Aggregate بررسی کنید آیا منطق Aggregate در صورت کار روی دادهٔ Eventually Consistent ممکن است State نامعتبر بسازد. در مثال واگذاری تیکت، اگر وضعیت «خوانده/نخوانده» پیامها با تأخیر بهروز شود، ممکن است تعداد زیادی تیکت بیدلیل واگذار شوند. بنابراین دادهٔ پیام باید در مرز Aggregate باشد.
ریشهٔ Aggregate
State Aggregate فقط با Commandهای خودش تغییر میکند. چون Aggregate سلسلهمراتبی از Entityهاست، فقط یکی از آنها باید بهعنوان رابط عمومی تعیین شود: Aggregate Root؛ شکل ۶-۵.
شکل ۶-۵ — ریشهٔ Aggregate.
نمونهٔ زیر نشان میدهد حتی وقتی Entity داخلی Message تغییر میکند، عملیات فقط از طریق ریشهٔ Ticket قابل دسترسی است:
public class Ticket
{
...
List<Message> _messages;
...
public void Execute(AcknowledgeMessage cmd)
{
var message = _messages.Where(x => x.Id == cmd.id).First();
message.WasRead = true;
}
...
}
رویدادهای دامنه (Domain Events)
علاوه بر رابط عمومی Aggregate Root، سازوکار دیگری برای ارتباط جهان بیرون با Aggregate وجود دارد: Domain Event.
Domain Event پیامی است که یک رویداد مهمِ رخداده در دامنهٔ کسبوکار را توصیف میکند، مانند:
- Ticket assigned
- Ticket escalated
- Message received
چون Domain Event چیزی را توصیف میکند که قبلاً رخ داده، نام آن باید در زمان گذشته باشد.
هدف رویداد دامنه این است که آنچه در کسبوکار رخ داده و دادههای لازم مرتبط با آن را منتقل کند. نمونه:
{
"ticket-id": "c9d286ff-3bca-4f57-94d4-4d4e490867d1",
"event-id": 146,
"event-type": "ticket-escalated",
"escalation-reason": "missed-sla",
"escalation-time": 1628970815
}
نامگذاری مهم است؛ نام Domain Event باید کوتاه و دقیقاً بازتابدهندهٔ اتفاق کسبوکاری باشد.
Domain Event بخشی از رابط عمومی Aggregate است. Aggregate رویدادهای خود را منتشر میکند و Processها، Aggregateهای دیگر یا حتی سیستمهای خارجی میتوانند Subscribe کنند و در پاسخ منطق خود را اجرا کنند؛ شکل ۶-۶.
شکل ۶-۶ — جریان انتشار Domain Event.
در نمونهٔ زیر رویداد جدید ساخته و به مجموعهٔ رویدادهای Ticket اضافه میشود:
01 public class Ticket
02 {
03 ...
04 private List<DomainEvent> _domainEvents;
05 ...
06
07 public void Execute(RequestEscalation cmd)
08 {
09 if (!this.IsEscalated && this.RemainingTimePercentage <= 0)
10 {
11 this.IsEscalated = true;
12 var escalatedEvent = new TicketEscalated(_id, cmd.Reason);
13 _domainEvents.Append(escalatedEvent);
14 }
15 }
16
17 ...
18 }
در فصل ۹ روش انتشار قابل اتکای Domain Eventها به مشترکان بررسی میشود.
زبان فراگیر و Aggregate
Aggregateها نیز باید زبان فراگیر را بازتاب دهند. نام Aggregate، اعضای دادهای، Actionها و Domain Eventها همگی باید با زبان فراگیر همان زمینهٔ محدود بیان شوند. به تعبیر Eric Evans، کد باید بر همان زبانی بنا شود که توسعهدهندگان هنگام صحبت با یکدیگر و خبرگان دامنه استفاده میکنند. این موضوع بهویژه برای منطق پیچیده اهمیت دارد.
سرویسهای دامنه (Domain Services)
دیر یا زود با منطقی روبهرو میشوید که نه به Aggregate خاصی تعلق دارد و نه به Value Object، یا ظاهراً به چند Aggregate مرتبط است. DDD پیشنهاد میکند چنین منطقی در Domain Service قرار گیرد.
Domain Service شیئی بدون State است که منطق کسبوکار را پیادهسازی میکند. در اغلب موارد، این منطق برای انجام محاسبه یا تحلیل، فراخوانی چند جزء سیستم را هماهنگ میکند.
به Ticket Aggregate برگردیم. Agent مهلت محدودی برای پاسخ دارد. این مهلت فقط به دادهٔ Ticket مثل Priority و Escalation وابسته نیست، بلکه به Policy واحد سازمانی برای SLA هر اولویت و Shiftهای کاری Agent نیز بستگی دارد؛ نمیتوان از Agent انتظار پاسخ خارج از ساعات کاری داشت.
محاسبهٔ Deadline نیازمند داده از Ticket، Department و Work Schedule است؛ بنابراین Domain Service مناسب است:
public class ResponseTimeFrameCalculationService
{
...
public ResponseTimeframe CalculateAgentResponseDeadline(UserId agentId,
Priority priority, bool escalated, DateTime startTime)
{
var policy = _departmentRepository.GetDepartmentPolicy(agentId);
var maxProcTime = policy.GetMaxResponseTimeFor(priority);
if (escalated) {
maxProcTime = maxProcTime * policy.EscalationFactor;
}
var shifts = _departmentRepository.GetUpcomingShifts(agentId,
startTime, startTime.Add(policy.MaxAgentResponseTime));
return CalculateTargetTime(maxProcTime, shifts);
}
...
}
Domain Serviceها هماهنگی کار چند Aggregate را آسان میکنند، اما راه فراری از قانون «فقط یک Aggregate در هر تراکنش» نیستند. این قانون همچنان برقرار است. Domain Service بیشتر برای منطق محاسباتیای مناسب است که باید دادهٔ چند Aggregate را بخواند.
همچنین Domain Service هیچ ارتباطی با Microservice، SOA یا دیگر کاربردهای واژهٔ Service ندارد؛ فقط یک شیء Stateless برای میزبانی منطق کسبوکار است.