[{"data":1,"prerenderedAt":3670},["ShallowReactive",2],{"content:\u002F06-architecture\u002F03-onion":3},{"title":4,"description":5,"path":6,"body":7},"Onion Architecture","","\u002F06-architecture\u002F03-onion",{"type":8,"value":9,"toc":3651},"minimark",[10,15,24,34,41,45,52,55,83,86,90,95,101,107,611,618,623,847,850,854,863,877,1062,1067,1071,1076,1088,1631,1634,1656,1660,1665,1866,2087,2091,2094,2196,2199,2204,2224,2229,2249,2253,2256,2318,2321,2509,2512,2677,2685,2689,2696,2699,2705,2708,2714,2717,2721,2724,2738,2741,2744,2846,2849,2852,2870,2874,2884,2902,2911,2917,2923,2929,2935,2944,2948,2953,2964,2969,2980,2985,2996,3001,3009,3013,3095,3099,3105,3111,3125,3135,3148,3168,3192,3202,3212,3218,3221,3225,3256,3472,3647],[11,12,14],"h2",{"id":13},"суть","Суть",[16,17,18,19,23],"p",{},"Hexagonal говорит \"используй порты и адаптеры\". Onion идёт дальше: ",[20,21,22],"strong",{},"внутри домена тоже есть слои",". Кольца, как у луковицы. Зависимости направлены строго внутрь. Автор — Jeffrey Palermo, 2008.",[25,26,31],"pre",{"className":27,"code":29,"language":30,"meta":5},[28],"language-text","        ┌────────────────────────────────────┐\n        │       Infrastructure               │\n        │  (postgres, http, redis, kafka)    │\n        │  ┌──────────────────────────────┐  │\n        │  │    Application Services      │  │\n        │  │  (use cases, оркестрация)    │  │\n        │  │  ┌────────────────────────┐  │  │\n        │  │  │   Domain Services      │  │  │\n        │  │  │  (логика между entity) │  │  │\n        │  │  │  ┌──────────────────┐  │  │  │\n        │  │  │  │  Domain Model    │  │  │  │\n        │  │  │  │ (entity, VO)     │  │  │  │\n        │  │  │  └──────────────────┘  │  │  │\n        │  │  └────────────────────────┘  │  │\n        │  └──────────────────────────────┘  │\n        └────────────────────────────────────┘\n","text",[32,33,29],"code",{"__ignoreMap":5},[16,35,36,37,40],{},"Domain Model в центре не импортирует ничего. Application — только Domain. Infrastructure — может импортировать всё внутри, но ",[20,38,39],{},"никто не импортирует Infrastructure",".",[11,42,44],{"id":43},"зачем-разбивать-домен-на-слои","Зачем разбивать домен на слои",[16,46,47,48,51],{},"В Hexagonal \"ядро\" — единый кусок. Доменные правила, оркестрация сценариев, сериализация в репозиторий — всё перемешано в ",[32,49,50],{},"OrderService",". Через год сервис на 2000 строк, и непонятно, где правило бизнеса, а где техническая склейка.",[16,53,54],{},"Onion разделяет:",[56,57,58,65,71,77],"ul",{},[59,60,61,64],"li",{},[20,62,63],{},"Что система знает"," — Domain Model (entity, value objects, инварианты)",[59,66,67,70],{},[20,68,69],{},"Что система умеет с собственным состоянием"," — Domain Services (логика, не помещающаяся в одну entity)",[59,72,73,76],{},[20,74,75],{},"Какие сценарии система выполняет"," — Application Services (use case)",[59,78,79,82],{},[20,80,81],{},"Как это делается технически"," — Infrastructure (БД, HTTP, очереди)",[16,84,85],{},"Каждый слой имеет свою ответственность и тестируется отдельно.",[11,87,89],{"id":88},"четыре-слоя","Четыре слоя",[91,92,94],"h3",{"id":93},"_1-domain-model-центр","1. Domain Model (центр)",[16,96,97,100],{},[20,98,99],{},"Что:"," entities, value objects, доменные ошибки, инварианты.",[16,102,103,106],{},[20,104,105],{},"Что НЕ импортирует:"," ничего внешнего. Ни ORM, ни json, ни logger.",[25,108,112],{"className":109,"code":110,"language":111,"meta":5,"style":5},"language-go shiki shiki-themes github-dark","\u002F\u002F domain\u002Forder.go\npackage domain\n\nimport \"errors\"\n\ntype OrderID string\n\ntype OrderStatus int\n\nconst (\n    StatusDraft OrderStatus = iota\n    StatusPaid\n    StatusCancelled\n)\n\ntype Order struct {\n    id     OrderID\n    items  []Item\n    status OrderStatus\n    total  Money\n}\n\nfunc NewOrder(id OrderID, items []Item) (*Order, error) {\n    if len(items) == 0 {\n        return nil, errors.New(\"order must have items\")\n    }\n    return &Order{id: id, items: items, status: StatusDraft, total: calcTotal(items)}, nil\n}\n\n\u002F\u002F Cancel — инвариант: нельзя отменить оплаченный заказ\nfunc (o *Order) Cancel() error {\n    if o.status == StatusPaid {\n        return errors.New(\"paid order cannot be cancelled\")\n    }\n    o.status = StatusCancelled\n    return nil\n}\n\nfunc (o *Order) Pay() error {\n    if o.status != StatusDraft {\n        return errors.New(\"only draft orders can be paid\")\n    }\n    o.status = StatusPaid\n    return nil\n}\n","go",[32,113,114,123,134,141,157,162,174,179,190,195,205,220,226,232,238,243,257,266,275,284,293,299,304,351,371,393,399,422,427,432,438,466,479,496,501,513,521,526,531,555,568,584,589,599,606],{"__ignoreMap":5},[115,116,119],"span",{"class":117,"line":118},"line",1,[115,120,122],{"class":121},"sAwPA","\u002F\u002F domain\u002Forder.go\n",[115,124,126,130],{"class":117,"line":125},2,[115,127,129],{"class":128},"snl16","package",[115,131,133],{"class":132},"svObZ"," domain\n",[115,135,137],{"class":117,"line":136},3,[115,138,140],{"emptyLinePlaceholder":139},true,"\n",[115,142,144,147,151,154],{"class":117,"line":143},4,[115,145,146],{"class":128},"import",[115,148,150],{"class":149},"sU2Wk"," \"",[115,152,153],{"class":132},"errors",[115,155,156],{"class":149},"\"\n",[115,158,160],{"class":117,"line":159},5,[115,161,140],{"emptyLinePlaceholder":139},[115,163,165,168,171],{"class":117,"line":164},6,[115,166,167],{"class":128},"type",[115,169,170],{"class":132}," OrderID",[115,172,173],{"class":128}," string\n",[115,175,177],{"class":117,"line":176},7,[115,178,140],{"emptyLinePlaceholder":139},[115,180,182,184,187],{"class":117,"line":181},8,[115,183,167],{"class":128},[115,185,186],{"class":132}," OrderStatus",[115,188,189],{"class":128}," int\n",[115,191,193],{"class":117,"line":192},9,[115,194,140],{"emptyLinePlaceholder":139},[115,196,198,201],{"class":117,"line":197},10,[115,199,200],{"class":128},"const",[115,202,204],{"class":203},"s95oV"," (\n",[115,206,208,212,214,217],{"class":117,"line":207},11,[115,209,211],{"class":210},"sDLfK","    StatusDraft",[115,213,186],{"class":132},[115,215,216],{"class":128}," =",[115,218,219],{"class":210}," iota\n",[115,221,223],{"class":117,"line":222},12,[115,224,225],{"class":210},"    StatusPaid\n",[115,227,229],{"class":117,"line":228},13,[115,230,231],{"class":210},"    StatusCancelled\n",[115,233,235],{"class":117,"line":234},14,[115,236,237],{"class":203},")\n",[115,239,241],{"class":117,"line":240},15,[115,242,140],{"emptyLinePlaceholder":139},[115,244,246,248,251,254],{"class":117,"line":245},16,[115,247,167],{"class":128},[115,249,250],{"class":132}," Order",[115,252,253],{"class":128}," struct",[115,255,256],{"class":203}," {\n",[115,258,260,263],{"class":117,"line":259},17,[115,261,262],{"class":203},"    id     ",[115,264,265],{"class":132},"OrderID\n",[115,267,269,272],{"class":117,"line":268},18,[115,270,271],{"class":203},"    items  []",[115,273,274],{"class":132},"Item\n",[115,276,278,281],{"class":117,"line":277},19,[115,279,280],{"class":203},"    status ",[115,282,283],{"class":132},"OrderStatus\n",[115,285,287,290],{"class":117,"line":286},20,[115,288,289],{"class":203},"    total  ",[115,291,292],{"class":132},"Money\n",[115,294,296],{"class":117,"line":295},21,[115,297,298],{"class":203},"}\n",[115,300,302],{"class":117,"line":301},22,[115,303,140],{"emptyLinePlaceholder":139},[115,305,307,310,313,316,320,322,325,328,331,334,337,340,343,345,348],{"class":117,"line":306},23,[115,308,309],{"class":128},"func",[115,311,312],{"class":132}," NewOrder",[115,314,315],{"class":203},"(",[115,317,319],{"class":318},"s9osk","id",[115,321,170],{"class":132},[115,323,324],{"class":203},", ",[115,326,327],{"class":318},"items",[115,329,330],{"class":203}," []",[115,332,333],{"class":132},"Item",[115,335,336],{"class":203},") (",[115,338,339],{"class":128},"*",[115,341,342],{"class":132},"Order",[115,344,324],{"class":203},[115,346,347],{"class":128},"error",[115,349,350],{"class":203},") {\n",[115,352,354,357,360,363,366,369],{"class":117,"line":353},24,[115,355,356],{"class":128},"    if",[115,358,359],{"class":132}," len",[115,361,362],{"class":203},"(items) ",[115,364,365],{"class":128},"==",[115,367,368],{"class":210}," 0",[115,370,256],{"class":203},[115,372,374,377,380,383,386,388,391],{"class":117,"line":373},25,[115,375,376],{"class":128},"        return",[115,378,379],{"class":210}," nil",[115,381,382],{"class":203},", errors.",[115,384,385],{"class":132},"New",[115,387,315],{"class":203},[115,389,390],{"class":149},"\"order must have items\"",[115,392,237],{"class":203},[115,394,396],{"class":117,"line":395},26,[115,397,398],{"class":203},"    }\n",[115,400,402,405,408,410,413,416,419],{"class":117,"line":401},27,[115,403,404],{"class":128},"    return",[115,406,407],{"class":128}," &",[115,409,342],{"class":132},[115,411,412],{"class":203},"{id: id, items: items, status: StatusDraft, total: ",[115,414,415],{"class":132},"calcTotal",[115,417,418],{"class":203},"(items)}, ",[115,420,421],{"class":210},"nil\n",[115,423,425],{"class":117,"line":424},28,[115,426,298],{"class":203},[115,428,430],{"class":117,"line":429},29,[115,431,140],{"emptyLinePlaceholder":139},[115,433,435],{"class":117,"line":434},30,[115,436,437],{"class":121},"\u002F\u002F Cancel — инвариант: нельзя отменить оплаченный заказ\n",[115,439,441,443,446,449,451,453,456,459,462,464],{"class":117,"line":440},31,[115,442,309],{"class":128},[115,444,445],{"class":203}," (",[115,447,448],{"class":318},"o ",[115,450,339],{"class":128},[115,452,342],{"class":132},[115,454,455],{"class":203},") ",[115,457,458],{"class":132},"Cancel",[115,460,461],{"class":203},"() ",[115,463,347],{"class":128},[115,465,256],{"class":203},[115,467,469,471,474,476],{"class":117,"line":468},32,[115,470,356],{"class":128},[115,472,473],{"class":203}," o.status ",[115,475,365],{"class":128},[115,477,478],{"class":203}," StatusPaid {\n",[115,480,482,484,487,489,491,494],{"class":117,"line":481},33,[115,483,376],{"class":128},[115,485,486],{"class":203}," errors.",[115,488,385],{"class":132},[115,490,315],{"class":203},[115,492,493],{"class":149},"\"paid order cannot be cancelled\"",[115,495,237],{"class":203},[115,497,499],{"class":117,"line":498},34,[115,500,398],{"class":203},[115,502,504,507,510],{"class":117,"line":503},35,[115,505,506],{"class":203},"    o.status ",[115,508,509],{"class":128},"=",[115,511,512],{"class":203}," StatusCancelled\n",[115,514,516,518],{"class":117,"line":515},36,[115,517,404],{"class":128},[115,519,520],{"class":210}," nil\n",[115,522,524],{"class":117,"line":523},37,[115,525,298],{"class":203},[115,527,529],{"class":117,"line":528},38,[115,530,140],{"emptyLinePlaceholder":139},[115,532,534,536,538,540,542,544,546,549,551,553],{"class":117,"line":533},39,[115,535,309],{"class":128},[115,537,445],{"class":203},[115,539,448],{"class":318},[115,541,339],{"class":128},[115,543,342],{"class":132},[115,545,455],{"class":203},[115,547,548],{"class":132},"Pay",[115,550,461],{"class":203},[115,552,347],{"class":128},[115,554,256],{"class":203},[115,556,558,560,562,565],{"class":117,"line":557},40,[115,559,356],{"class":128},[115,561,473],{"class":203},[115,563,564],{"class":128},"!=",[115,566,567],{"class":203}," StatusDraft {\n",[115,569,571,573,575,577,579,582],{"class":117,"line":570},41,[115,572,376],{"class":128},[115,574,486],{"class":203},[115,576,385],{"class":132},[115,578,315],{"class":203},[115,580,581],{"class":149},"\"only draft orders can be paid\"",[115,583,237],{"class":203},[115,585,587],{"class":117,"line":586},42,[115,588,398],{"class":203},[115,590,592,594,596],{"class":117,"line":591},43,[115,593,506],{"class":203},[115,595,509],{"class":128},[115,597,598],{"class":203}," StatusPaid\n",[115,600,602,604],{"class":117,"line":601},44,[115,603,404],{"class":128},[115,605,520],{"class":210},[115,607,609],{"class":117,"line":608},45,[115,610,298],{"class":203},[16,612,613,614,617],{},"Инкапсуляция: поля приватные, изменения через методы, внутри — проверка инвариантов. Это ",[20,615,616],{},"богатая модель"," (rich domain model), а не DTO.",[16,619,620],{},[20,621,622],{},"Value Object:",[25,624,626],{"className":109,"code":625,"language":111,"meta":5,"style":5},"\u002F\u002F domain\u002Fmoney.go\ntype Money struct {\n    amount   int64  \u002F\u002F в копейках\n    currency string\n}\n\nfunc NewMoney(amount int64, currency string) (Money, error) {\n    if amount \u003C 0 {\n        return Money{}, errors.New(\"negative amount\")\n    }\n    return Money{amount: amount, currency: currency}, nil\n}\n\nfunc (m Money) Add(other Money) (Money, error) {\n    if m.currency != other.currency {\n        return Money{}, errors.New(\"currency mismatch\")\n    }\n    return Money{amount: m.amount + other.amount, currency: m.currency}, nil\n}\n",[32,627,628,633,644,655,663,667,671,705,719,737,741,752,756,760,793,805,822,826,843],{"__ignoreMap":5},[115,629,630],{"class":117,"line":118},[115,631,632],{"class":121},"\u002F\u002F domain\u002Fmoney.go\n",[115,634,635,637,640,642],{"class":117,"line":125},[115,636,167],{"class":128},[115,638,639],{"class":132}," Money",[115,641,253],{"class":128},[115,643,256],{"class":203},[115,645,646,649,652],{"class":117,"line":136},[115,647,648],{"class":203},"    amount   ",[115,650,651],{"class":128},"int64",[115,653,654],{"class":121},"  \u002F\u002F в копейках\n",[115,656,657,660],{"class":117,"line":143},[115,658,659],{"class":203},"    currency ",[115,661,662],{"class":128},"string\n",[115,664,665],{"class":117,"line":159},[115,666,298],{"class":203},[115,668,669],{"class":117,"line":164},[115,670,140],{"emptyLinePlaceholder":139},[115,672,673,675,678,680,683,686,688,691,694,696,699,701,703],{"class":117,"line":176},[115,674,309],{"class":128},[115,676,677],{"class":132}," NewMoney",[115,679,315],{"class":203},[115,681,682],{"class":318},"amount",[115,684,685],{"class":128}," int64",[115,687,324],{"class":203},[115,689,690],{"class":318},"currency",[115,692,693],{"class":128}," string",[115,695,336],{"class":203},[115,697,698],{"class":132},"Money",[115,700,324],{"class":203},[115,702,347],{"class":128},[115,704,350],{"class":203},[115,706,707,709,712,715,717],{"class":117,"line":181},[115,708,356],{"class":128},[115,710,711],{"class":203}," amount ",[115,713,714],{"class":128},"\u003C",[115,716,368],{"class":210},[115,718,256],{"class":203},[115,720,721,723,725,728,730,732,735],{"class":117,"line":192},[115,722,376],{"class":128},[115,724,639],{"class":132},[115,726,727],{"class":203},"{}, errors.",[115,729,385],{"class":132},[115,731,315],{"class":203},[115,733,734],{"class":149},"\"negative amount\"",[115,736,237],{"class":203},[115,738,739],{"class":117,"line":197},[115,740,398],{"class":203},[115,742,743,745,747,750],{"class":117,"line":207},[115,744,404],{"class":128},[115,746,639],{"class":132},[115,748,749],{"class":203},"{amount: amount, currency: currency}, ",[115,751,421],{"class":210},[115,753,754],{"class":117,"line":222},[115,755,298],{"class":203},[115,757,758],{"class":117,"line":228},[115,759,140],{"emptyLinePlaceholder":139},[115,761,762,764,766,769,771,773,776,778,781,783,785,787,789,791],{"class":117,"line":234},[115,763,309],{"class":128},[115,765,445],{"class":203},[115,767,768],{"class":318},"m ",[115,770,698],{"class":132},[115,772,455],{"class":203},[115,774,775],{"class":132},"Add",[115,777,315],{"class":203},[115,779,780],{"class":318},"other",[115,782,639],{"class":132},[115,784,336],{"class":203},[115,786,698],{"class":132},[115,788,324],{"class":203},[115,790,347],{"class":128},[115,792,350],{"class":203},[115,794,795,797,800,802],{"class":117,"line":240},[115,796,356],{"class":128},[115,798,799],{"class":203}," m.currency ",[115,801,564],{"class":128},[115,803,804],{"class":203}," other.currency {\n",[115,806,807,809,811,813,815,817,820],{"class":117,"line":245},[115,808,376],{"class":128},[115,810,639],{"class":132},[115,812,727],{"class":203},[115,814,385],{"class":132},[115,816,315],{"class":203},[115,818,819],{"class":149},"\"currency mismatch\"",[115,821,237],{"class":203},[115,823,824],{"class":117,"line":259},[115,825,398],{"class":203},[115,827,828,830,832,835,838,841],{"class":117,"line":268},[115,829,404],{"class":128},[115,831,639],{"class":132},[115,833,834],{"class":203},"{amount: m.amount ",[115,836,837],{"class":128},"+",[115,839,840],{"class":203}," other.amount, currency: m.currency}, ",[115,842,421],{"class":210},[115,844,845],{"class":117,"line":277},[115,846,298],{"class":203},[16,848,849],{},"Value object иммутабельный, сравнивается по значению, проверяет свои инварианты.",[91,851,853],{"id":852},"_2-domain-services","2. Domain Services",[16,855,856,858,859,862],{},[20,857,99],{}," логика, которая работает с ",[20,860,861],{},"несколькими entity"," или не вписывается в одну. Чисто доменная, без оркестрации.",[16,864,865,868,869,872,873,876],{},[20,866,867],{},"Когда нужен:"," перевод денег между двумя счетами (не помещается ни в ",[32,870,871],{},"Account.Send",", ни в ",[32,874,875],{},"Account.Receive"," — обе entity участвуют).",[25,878,880],{"className":109,"code":879,"language":111,"meta":5,"style":5},"\u002F\u002F domain\u002Ftransfer_service.go\npackage domain\n\ntype TransferService struct{}\n\n\u002F\u002F TransferMoney — чистая доменная логика, без БД и транзакций\nfunc (s *TransferService) Transfer(from, to *Account, amount Money) error {\n    if err := from.Withdraw(amount); err != nil {\n        return err\n    }\n    if err := to.Deposit(amount); err != nil {\n        \u002F\u002F компенсация — снова положить\n        _ = from.Deposit(amount)\n        return err\n    }\n    return nil\n}\n",[32,881,882,887,893,897,909,913,918,965,990,997,1001,1023,1028,1042,1048,1052,1058],{"__ignoreMap":5},[115,883,884],{"class":117,"line":118},[115,885,886],{"class":121},"\u002F\u002F domain\u002Ftransfer_service.go\n",[115,888,889,891],{"class":117,"line":125},[115,890,129],{"class":128},[115,892,133],{"class":132},[115,894,895],{"class":117,"line":136},[115,896,140],{"emptyLinePlaceholder":139},[115,898,899,901,904,906],{"class":117,"line":143},[115,900,167],{"class":128},[115,902,903],{"class":132}," TransferService",[115,905,253],{"class":128},[115,907,908],{"class":203},"{}\n",[115,910,911],{"class":117,"line":159},[115,912,140],{"emptyLinePlaceholder":139},[115,914,915],{"class":117,"line":164},[115,916,917],{"class":121},"\u002F\u002F TransferMoney — чистая доменная логика, без БД и транзакций\n",[115,919,920,922,924,927,929,932,934,937,939,942,944,947,950,953,955,957,959,961,963],{"class":117,"line":176},[115,921,309],{"class":128},[115,923,445],{"class":203},[115,925,926],{"class":318},"s ",[115,928,339],{"class":128},[115,930,931],{"class":132},"TransferService",[115,933,455],{"class":203},[115,935,936],{"class":132},"Transfer",[115,938,315],{"class":203},[115,940,941],{"class":318},"from",[115,943,324],{"class":203},[115,945,946],{"class":318},"to",[115,948,949],{"class":128}," *",[115,951,952],{"class":132},"Account",[115,954,324],{"class":203},[115,956,682],{"class":318},[115,958,639],{"class":132},[115,960,455],{"class":203},[115,962,347],{"class":128},[115,964,256],{"class":203},[115,966,967,969,972,975,978,981,984,986,988],{"class":117,"line":181},[115,968,356],{"class":128},[115,970,971],{"class":203}," err ",[115,973,974],{"class":128},":=",[115,976,977],{"class":203}," from.",[115,979,980],{"class":132},"Withdraw",[115,982,983],{"class":203},"(amount); err ",[115,985,564],{"class":128},[115,987,379],{"class":210},[115,989,256],{"class":203},[115,991,992,994],{"class":117,"line":192},[115,993,376],{"class":128},[115,995,996],{"class":203}," err\n",[115,998,999],{"class":117,"line":197},[115,1000,398],{"class":203},[115,1002,1003,1005,1007,1009,1012,1015,1017,1019,1021],{"class":117,"line":207},[115,1004,356],{"class":128},[115,1006,971],{"class":203},[115,1008,974],{"class":128},[115,1010,1011],{"class":203}," to.",[115,1013,1014],{"class":132},"Deposit",[115,1016,983],{"class":203},[115,1018,564],{"class":128},[115,1020,379],{"class":210},[115,1022,256],{"class":203},[115,1024,1025],{"class":117,"line":222},[115,1026,1027],{"class":121},"        \u002F\u002F компенсация — снова положить\n",[115,1029,1030,1033,1035,1037,1039],{"class":117,"line":228},[115,1031,1032],{"class":203},"        _ ",[115,1034,509],{"class":128},[115,1036,977],{"class":203},[115,1038,1014],{"class":132},[115,1040,1041],{"class":203},"(amount)\n",[115,1043,1044,1046],{"class":117,"line":234},[115,1045,376],{"class":128},[115,1047,996],{"class":203},[115,1049,1050],{"class":117,"line":240},[115,1051,398],{"class":203},[115,1053,1054,1056],{"class":117,"line":245},[115,1055,404],{"class":128},[115,1057,520],{"class":210},[115,1059,1060],{"class":117,"line":259},[115,1061,298],{"class":203},[16,1063,1064,1066],{},[32,1065,931],{}," ничего не знает про БД и транзакции. Только бизнес-правило: \"снять с одного, положить на другой\".",[91,1068,1070],{"id":1069},"_3-application-services-use-cases","3. Application Services (Use Cases)",[16,1072,1073,1075],{},[20,1074,99],{}," оркестрация сценария use case. Тянет данные из репо, дёргает домен, сохраняет, шлёт события.",[16,1077,1078,1080,1081,1084,1085,1087],{},[20,1079,867],{}," \"перевести деньги по REST-запросу\" — это не доменная операция, это ",[20,1082,1083],{},"сценарий",": загрузить два счёта, дёрнуть ",[32,1086,931],{},", сохранить оба, опубликовать событие.",[25,1089,1091],{"className":109,"code":1090,"language":111,"meta":5,"style":5},"\u002F\u002F application\u002Ftransfer_money.go\npackage application\n\nimport \"myapp\u002Fdomain\"\n\ntype AccountRepo interface {\n    FindByID(ctx context.Context, id domain.AccountID) (*domain.Account, error)\n    Save(ctx context.Context, a *domain.Account) error\n}\n\ntype EventBus interface {\n    Publish(ctx context.Context, event any) error\n}\n\ntype TransferMoneyHandler struct {\n    repo     AccountRepo\n    bus      EventBus\n    transfer *domain.TransferService\n    tx       UnitOfWork\n}\n\nfunc (h *TransferMoneyHandler) Execute(ctx context.Context, fromID, toID domain.AccountID, amount domain.Money) error {\n    return h.tx.Run(ctx, func(ctx context.Context) error {\n        from, err := h.repo.FindByID(ctx, fromID)\n        if err != nil {\n            return err\n        }\n        to, err := h.repo.FindByID(ctx, toID)\n        if err != nil {\n            return err\n        }\n\n        if err := h.transfer.Transfer(from, to, amount); err != nil {\n            return err\n        }\n\n        if err := h.repo.Save(ctx, from); err != nil {\n            return err\n        }\n        if err := h.repo.Save(ctx, to); err != nil {\n            return err\n        }\n\n        return h.bus.Publish(ctx, MoneyTransferred{From: fromID, To: toID, Amount: amount})\n    })\n}\n",[32,1092,1093,1098,1105,1109,1120,1124,1136,1183,1216,1220,1224,1235,1262,1266,1270,1281,1289,1297,1311,1319,1323,1327,1388,1419,1435,1448,1455,1460,1474,1486,1492,1496,1500,1522,1528,1532,1536,1558,1564,1568,1589,1595,1599,1603,1621,1626],{"__ignoreMap":5},[115,1094,1095],{"class":117,"line":118},[115,1096,1097],{"class":121},"\u002F\u002F application\u002Ftransfer_money.go\n",[115,1099,1100,1102],{"class":117,"line":125},[115,1101,129],{"class":128},[115,1103,1104],{"class":132}," application\n",[115,1106,1107],{"class":117,"line":136},[115,1108,140],{"emptyLinePlaceholder":139},[115,1110,1111,1113,1115,1118],{"class":117,"line":143},[115,1112,146],{"class":128},[115,1114,150],{"class":149},[115,1116,1117],{"class":132},"myapp\u002Fdomain",[115,1119,156],{"class":149},[115,1121,1122],{"class":117,"line":159},[115,1123,140],{"emptyLinePlaceholder":139},[115,1125,1126,1128,1131,1134],{"class":117,"line":164},[115,1127,167],{"class":128},[115,1129,1130],{"class":132}," AccountRepo",[115,1132,1133],{"class":128}," interface",[115,1135,256],{"class":203},[115,1137,1138,1141,1143,1146,1149,1151,1154,1156,1158,1161,1163,1166,1168,1170,1173,1175,1177,1179,1181],{"class":117,"line":176},[115,1139,1140],{"class":132},"    FindByID",[115,1142,315],{"class":203},[115,1144,1145],{"class":318},"ctx",[115,1147,1148],{"class":132}," context",[115,1150,40],{"class":203},[115,1152,1153],{"class":132},"Context",[115,1155,324],{"class":203},[115,1157,319],{"class":318},[115,1159,1160],{"class":132}," domain",[115,1162,40],{"class":203},[115,1164,1165],{"class":132},"AccountID",[115,1167,336],{"class":203},[115,1169,339],{"class":128},[115,1171,1172],{"class":132},"domain",[115,1174,40],{"class":203},[115,1176,952],{"class":132},[115,1178,324],{"class":203},[115,1180,347],{"class":128},[115,1182,237],{"class":203},[115,1184,1185,1188,1190,1192,1194,1196,1198,1200,1203,1205,1207,1209,1211,1213],{"class":117,"line":181},[115,1186,1187],{"class":132},"    Save",[115,1189,315],{"class":203},[115,1191,1145],{"class":318},[115,1193,1148],{"class":132},[115,1195,40],{"class":203},[115,1197,1153],{"class":132},[115,1199,324],{"class":203},[115,1201,1202],{"class":318},"a",[115,1204,949],{"class":128},[115,1206,1172],{"class":132},[115,1208,40],{"class":203},[115,1210,952],{"class":132},[115,1212,455],{"class":203},[115,1214,1215],{"class":128},"error\n",[115,1217,1218],{"class":117,"line":192},[115,1219,298],{"class":203},[115,1221,1222],{"class":117,"line":197},[115,1223,140],{"emptyLinePlaceholder":139},[115,1225,1226,1228,1231,1233],{"class":117,"line":207},[115,1227,167],{"class":128},[115,1229,1230],{"class":132}," EventBus",[115,1232,1133],{"class":128},[115,1234,256],{"class":203},[115,1236,1237,1240,1242,1244,1246,1248,1250,1252,1255,1258,1260],{"class":117,"line":222},[115,1238,1239],{"class":132},"    Publish",[115,1241,315],{"class":203},[115,1243,1145],{"class":318},[115,1245,1148],{"class":132},[115,1247,40],{"class":203},[115,1249,1153],{"class":132},[115,1251,324],{"class":203},[115,1253,1254],{"class":318},"event",[115,1256,1257],{"class":132}," any",[115,1259,455],{"class":203},[115,1261,1215],{"class":128},[115,1263,1264],{"class":117,"line":228},[115,1265,298],{"class":203},[115,1267,1268],{"class":117,"line":234},[115,1269,140],{"emptyLinePlaceholder":139},[115,1271,1272,1274,1277,1279],{"class":117,"line":240},[115,1273,167],{"class":128},[115,1275,1276],{"class":132}," TransferMoneyHandler",[115,1278,253],{"class":128},[115,1280,256],{"class":203},[115,1282,1283,1286],{"class":117,"line":245},[115,1284,1285],{"class":203},"    repo     ",[115,1287,1288],{"class":132},"AccountRepo\n",[115,1290,1291,1294],{"class":117,"line":259},[115,1292,1293],{"class":203},"    bus      ",[115,1295,1296],{"class":132},"EventBus\n",[115,1298,1299,1302,1304,1306,1308],{"class":117,"line":268},[115,1300,1301],{"class":203},"    transfer ",[115,1303,339],{"class":128},[115,1305,1172],{"class":132},[115,1307,40],{"class":203},[115,1309,1310],{"class":132},"TransferService\n",[115,1312,1313,1316],{"class":117,"line":277},[115,1314,1315],{"class":203},"    tx       ",[115,1317,1318],{"class":132},"UnitOfWork\n",[115,1320,1321],{"class":117,"line":286},[115,1322,298],{"class":203},[115,1324,1325],{"class":117,"line":295},[115,1326,140],{"emptyLinePlaceholder":139},[115,1328,1329,1331,1333,1336,1338,1341,1343,1346,1348,1350,1352,1354,1356,1358,1361,1363,1366,1368,1370,1372,1374,1376,1378,1380,1382,1384,1386],{"class":117,"line":301},[115,1330,309],{"class":128},[115,1332,445],{"class":203},[115,1334,1335],{"class":318},"h ",[115,1337,339],{"class":128},[115,1339,1340],{"class":132},"TransferMoneyHandler",[115,1342,455],{"class":203},[115,1344,1345],{"class":132},"Execute",[115,1347,315],{"class":203},[115,1349,1145],{"class":318},[115,1351,1148],{"class":132},[115,1353,40],{"class":203},[115,1355,1153],{"class":132},[115,1357,324],{"class":203},[115,1359,1360],{"class":318},"fromID",[115,1362,324],{"class":203},[115,1364,1365],{"class":318},"toID",[115,1367,1160],{"class":132},[115,1369,40],{"class":203},[115,1371,1165],{"class":132},[115,1373,324],{"class":203},[115,1375,682],{"class":318},[115,1377,1160],{"class":132},[115,1379,40],{"class":203},[115,1381,698],{"class":132},[115,1383,455],{"class":203},[115,1385,347],{"class":128},[115,1387,256],{"class":203},[115,1389,1390,1392,1395,1398,1401,1403,1405,1407,1409,1411,1413,1415,1417],{"class":117,"line":306},[115,1391,404],{"class":128},[115,1393,1394],{"class":203}," h.tx.",[115,1396,1397],{"class":132},"Run",[115,1399,1400],{"class":203},"(ctx, ",[115,1402,309],{"class":128},[115,1404,315],{"class":203},[115,1406,1145],{"class":318},[115,1408,1148],{"class":132},[115,1410,40],{"class":203},[115,1412,1153],{"class":132},[115,1414,455],{"class":203},[115,1416,347],{"class":128},[115,1418,256],{"class":203},[115,1420,1421,1424,1426,1429,1432],{"class":117,"line":353},[115,1422,1423],{"class":203},"        from, err ",[115,1425,974],{"class":128},[115,1427,1428],{"class":203}," h.repo.",[115,1430,1431],{"class":132},"FindByID",[115,1433,1434],{"class":203},"(ctx, fromID)\n",[115,1436,1437,1440,1442,1444,1446],{"class":117,"line":373},[115,1438,1439],{"class":128},"        if",[115,1441,971],{"class":203},[115,1443,564],{"class":128},[115,1445,379],{"class":210},[115,1447,256],{"class":203},[115,1449,1450,1453],{"class":117,"line":395},[115,1451,1452],{"class":128},"            return",[115,1454,996],{"class":203},[115,1456,1457],{"class":117,"line":401},[115,1458,1459],{"class":203},"        }\n",[115,1461,1462,1465,1467,1469,1471],{"class":117,"line":424},[115,1463,1464],{"class":203},"        to, err ",[115,1466,974],{"class":128},[115,1468,1428],{"class":203},[115,1470,1431],{"class":132},[115,1472,1473],{"class":203},"(ctx, toID)\n",[115,1475,1476,1478,1480,1482,1484],{"class":117,"line":429},[115,1477,1439],{"class":128},[115,1479,971],{"class":203},[115,1481,564],{"class":128},[115,1483,379],{"class":210},[115,1485,256],{"class":203},[115,1487,1488,1490],{"class":117,"line":434},[115,1489,1452],{"class":128},[115,1491,996],{"class":203},[115,1493,1494],{"class":117,"line":440},[115,1495,1459],{"class":203},[115,1497,1498],{"class":117,"line":468},[115,1499,140],{"emptyLinePlaceholder":139},[115,1501,1502,1504,1506,1508,1511,1513,1516,1518,1520],{"class":117,"line":481},[115,1503,1439],{"class":128},[115,1505,971],{"class":203},[115,1507,974],{"class":128},[115,1509,1510],{"class":203}," h.transfer.",[115,1512,936],{"class":132},[115,1514,1515],{"class":203},"(from, to, amount); err ",[115,1517,564],{"class":128},[115,1519,379],{"class":210},[115,1521,256],{"class":203},[115,1523,1524,1526],{"class":117,"line":498},[115,1525,1452],{"class":128},[115,1527,996],{"class":203},[115,1529,1530],{"class":117,"line":503},[115,1531,1459],{"class":203},[115,1533,1534],{"class":117,"line":515},[115,1535,140],{"emptyLinePlaceholder":139},[115,1537,1538,1540,1542,1544,1546,1549,1552,1554,1556],{"class":117,"line":523},[115,1539,1439],{"class":128},[115,1541,971],{"class":203},[115,1543,974],{"class":128},[115,1545,1428],{"class":203},[115,1547,1548],{"class":132},"Save",[115,1550,1551],{"class":203},"(ctx, from); err ",[115,1553,564],{"class":128},[115,1555,379],{"class":210},[115,1557,256],{"class":203},[115,1559,1560,1562],{"class":117,"line":528},[115,1561,1452],{"class":128},[115,1563,996],{"class":203},[115,1565,1566],{"class":117,"line":533},[115,1567,1459],{"class":203},[115,1569,1570,1572,1574,1576,1578,1580,1583,1585,1587],{"class":117,"line":557},[115,1571,1439],{"class":128},[115,1573,971],{"class":203},[115,1575,974],{"class":128},[115,1577,1428],{"class":203},[115,1579,1548],{"class":132},[115,1581,1582],{"class":203},"(ctx, to); err ",[115,1584,564],{"class":128},[115,1586,379],{"class":210},[115,1588,256],{"class":203},[115,1590,1591,1593],{"class":117,"line":570},[115,1592,1452],{"class":128},[115,1594,996],{"class":203},[115,1596,1597],{"class":117,"line":586},[115,1598,1459],{"class":203},[115,1600,1601],{"class":117,"line":591},[115,1602,140],{"emptyLinePlaceholder":139},[115,1604,1605,1607,1610,1613,1615,1618],{"class":117,"line":601},[115,1606,376],{"class":128},[115,1608,1609],{"class":203}," h.bus.",[115,1611,1612],{"class":132},"Publish",[115,1614,1400],{"class":203},[115,1616,1617],{"class":132},"MoneyTransferred",[115,1619,1620],{"class":203},"{From: fromID, To: toID, Amount: amount})\n",[115,1622,1623],{"class":117,"line":608},[115,1624,1625],{"class":203},"    })\n",[115,1627,1629],{"class":117,"line":1628},46,[115,1630,298],{"class":203},[16,1632,1633],{},"Application Service:",[56,1635,1636,1639,1642,1645],{},[59,1637,1638],{},"Знает порядок шагов",[59,1640,1641],{},"Управляет транзакцией",[59,1643,1644],{},"Дёргает Domain",[59,1646,1647,1648,1651,1652,1655],{},"Не содержит бизнес-правил (правила — в ",[32,1649,1650],{},"Account.Withdraw"," и ",[32,1653,1654],{},"TransferService.Transfer",")",[91,1657,1659],{"id":1658},"_4-infrastructure","4. Infrastructure",[16,1661,1662,1664],{},[20,1663,99],{}," реализации портов. Postgres, http-handler, kafka-producer, redis-cache.",[25,1666,1668],{"className":109,"code":1667,"language":111,"meta":5,"style":5},"\u002F\u002F infrastructure\u002Fpostgres\u002Faccount_repo.go\npackage postgres\n\ntype AccountRepo struct{ db *pgxpool.Pool }\n\nfunc (r *AccountRepo) FindByID(ctx context.Context, id domain.AccountID) (*domain.Account, error) {\n    row := r.db.QueryRow(ctx, \"SELECT id, balance FROM accounts WHERE id = $1\", id)\n    var dto accountRow\n    if err := row.Scan(&dto.ID, &dto.Balance); err != nil {\n        return nil, err\n    }\n    return mapToDomain(dto), nil \u002F\u002F маппинг row → entity на границе\n}\n",[32,1669,1670,1675,1682,1686,1710,1714,1768,1789,1800,1833,1842,1846,1862],{"__ignoreMap":5},[115,1671,1672],{"class":117,"line":118},[115,1673,1674],{"class":121},"\u002F\u002F infrastructure\u002Fpostgres\u002Faccount_repo.go\n",[115,1676,1677,1679],{"class":117,"line":125},[115,1678,129],{"class":128},[115,1680,1681],{"class":132}," postgres\n",[115,1683,1684],{"class":117,"line":136},[115,1685,140],{"emptyLinePlaceholder":139},[115,1687,1688,1690,1692,1694,1697,1699,1702,1704,1707],{"class":117,"line":143},[115,1689,167],{"class":128},[115,1691,1130],{"class":132},[115,1693,253],{"class":128},[115,1695,1696],{"class":203},"{ db ",[115,1698,339],{"class":128},[115,1700,1701],{"class":132},"pgxpool",[115,1703,40],{"class":203},[115,1705,1706],{"class":132},"Pool",[115,1708,1709],{"class":203}," }\n",[115,1711,1712],{"class":117,"line":159},[115,1713,140],{"emptyLinePlaceholder":139},[115,1715,1716,1718,1720,1723,1725,1728,1730,1732,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752,1754,1756,1758,1760,1762,1764,1766],{"class":117,"line":164},[115,1717,309],{"class":128},[115,1719,445],{"class":203},[115,1721,1722],{"class":318},"r ",[115,1724,339],{"class":128},[115,1726,1727],{"class":132},"AccountRepo",[115,1729,455],{"class":203},[115,1731,1431],{"class":132},[115,1733,315],{"class":203},[115,1735,1145],{"class":318},[115,1737,1148],{"class":132},[115,1739,40],{"class":203},[115,1741,1153],{"class":132},[115,1743,324],{"class":203},[115,1745,319],{"class":318},[115,1747,1160],{"class":132},[115,1749,40],{"class":203},[115,1751,1165],{"class":132},[115,1753,336],{"class":203},[115,1755,339],{"class":128},[115,1757,1172],{"class":132},[115,1759,40],{"class":203},[115,1761,952],{"class":132},[115,1763,324],{"class":203},[115,1765,347],{"class":128},[115,1767,350],{"class":203},[115,1769,1770,1773,1775,1778,1781,1783,1786],{"class":117,"line":176},[115,1771,1772],{"class":203},"    row ",[115,1774,974],{"class":128},[115,1776,1777],{"class":203}," r.db.",[115,1779,1780],{"class":132},"QueryRow",[115,1782,1400],{"class":203},[115,1784,1785],{"class":149},"\"SELECT id, balance FROM accounts WHERE id = $1\"",[115,1787,1788],{"class":203},", id)\n",[115,1790,1791,1794,1797],{"class":117,"line":181},[115,1792,1793],{"class":128},"    var",[115,1795,1796],{"class":203}," dto ",[115,1798,1799],{"class":132},"accountRow\n",[115,1801,1802,1804,1806,1808,1811,1814,1816,1819,1822,1824,1827,1829,1831],{"class":117,"line":192},[115,1803,356],{"class":128},[115,1805,971],{"class":203},[115,1807,974],{"class":128},[115,1809,1810],{"class":203}," row.",[115,1812,1813],{"class":132},"Scan",[115,1815,315],{"class":203},[115,1817,1818],{"class":128},"&",[115,1820,1821],{"class":203},"dto.ID, ",[115,1823,1818],{"class":128},[115,1825,1826],{"class":203},"dto.Balance); err ",[115,1828,564],{"class":128},[115,1830,379],{"class":210},[115,1832,256],{"class":203},[115,1834,1835,1837,1839],{"class":117,"line":197},[115,1836,376],{"class":128},[115,1838,379],{"class":210},[115,1840,1841],{"class":203},", err\n",[115,1843,1844],{"class":117,"line":207},[115,1845,398],{"class":203},[115,1847,1848,1850,1853,1856,1859],{"class":117,"line":222},[115,1849,404],{"class":128},[115,1851,1852],{"class":132}," mapToDomain",[115,1854,1855],{"class":203},"(dto), ",[115,1857,1858],{"class":210},"nil",[115,1860,1861],{"class":121}," \u002F\u002F маппинг row → entity на границе\n",[115,1863,1864],{"class":117,"line":228},[115,1865,298],{"class":203},[25,1867,1869],{"className":109,"code":1868,"language":111,"meta":5,"style":5},"\u002F\u002F infrastructure\u002Fhttp\u002Ftransfer_handler.go\npackage http\n\ntype TransferHandler struct {\n    handler *application.TransferMoneyHandler\n}\n\nfunc (h *TransferHandler) Handle(c echo.Context) error {\n    var req TransferRequest\n    if err := c.Bind(&req); err != nil {\n        return c.JSON(400, err)\n    }\n    if err := h.handler.Execute(c.Request().Context(), req.From, req.To, req.Amount); err != nil {\n        return c.JSON(500, err)\n    }\n    return c.NoContent(204)\n}\n",[32,1870,1871,1876,1883,1887,1898,1913,1917,1921,1957,1967,1994,2011,2015,2048,2063,2067,2083],{"__ignoreMap":5},[115,1872,1873],{"class":117,"line":118},[115,1874,1875],{"class":121},"\u002F\u002F infrastructure\u002Fhttp\u002Ftransfer_handler.go\n",[115,1877,1878,1880],{"class":117,"line":125},[115,1879,129],{"class":128},[115,1881,1882],{"class":132}," http\n",[115,1884,1885],{"class":117,"line":136},[115,1886,140],{"emptyLinePlaceholder":139},[115,1888,1889,1891,1894,1896],{"class":117,"line":143},[115,1890,167],{"class":128},[115,1892,1893],{"class":132}," TransferHandler",[115,1895,253],{"class":128},[115,1897,256],{"class":203},[115,1899,1900,1903,1905,1908,1910],{"class":117,"line":159},[115,1901,1902],{"class":203},"    handler ",[115,1904,339],{"class":128},[115,1906,1907],{"class":132},"application",[115,1909,40],{"class":203},[115,1911,1912],{"class":132},"TransferMoneyHandler\n",[115,1914,1915],{"class":117,"line":164},[115,1916,298],{"class":203},[115,1918,1919],{"class":117,"line":176},[115,1920,140],{"emptyLinePlaceholder":139},[115,1922,1923,1925,1927,1929,1931,1934,1936,1939,1941,1944,1947,1949,1951,1953,1955],{"class":117,"line":181},[115,1924,309],{"class":128},[115,1926,445],{"class":203},[115,1928,1335],{"class":318},[115,1930,339],{"class":128},[115,1932,1933],{"class":132},"TransferHandler",[115,1935,455],{"class":203},[115,1937,1938],{"class":132},"Handle",[115,1940,315],{"class":203},[115,1942,1943],{"class":318},"c",[115,1945,1946],{"class":132}," echo",[115,1948,40],{"class":203},[115,1950,1153],{"class":132},[115,1952,455],{"class":203},[115,1954,347],{"class":128},[115,1956,256],{"class":203},[115,1958,1959,1961,1964],{"class":117,"line":192},[115,1960,1793],{"class":128},[115,1962,1963],{"class":203}," req ",[115,1965,1966],{"class":132},"TransferRequest\n",[115,1968,1969,1971,1973,1975,1978,1981,1983,1985,1988,1990,1992],{"class":117,"line":197},[115,1970,356],{"class":128},[115,1972,971],{"class":203},[115,1974,974],{"class":128},[115,1976,1977],{"class":203}," c.",[115,1979,1980],{"class":132},"Bind",[115,1982,315],{"class":203},[115,1984,1818],{"class":128},[115,1986,1987],{"class":203},"req); err ",[115,1989,564],{"class":128},[115,1991,379],{"class":210},[115,1993,256],{"class":203},[115,1995,1996,1998,2000,2003,2005,2008],{"class":117,"line":207},[115,1997,376],{"class":128},[115,1999,1977],{"class":203},[115,2001,2002],{"class":132},"JSON",[115,2004,315],{"class":203},[115,2006,2007],{"class":210},"400",[115,2009,2010],{"class":203},", err)\n",[115,2012,2013],{"class":117,"line":222},[115,2014,398],{"class":203},[115,2016,2017,2019,2021,2023,2026,2028,2031,2034,2037,2039,2042,2044,2046],{"class":117,"line":228},[115,2018,356],{"class":128},[115,2020,971],{"class":203},[115,2022,974],{"class":128},[115,2024,2025],{"class":203}," h.handler.",[115,2027,1345],{"class":132},[115,2029,2030],{"class":203},"(c.",[115,2032,2033],{"class":132},"Request",[115,2035,2036],{"class":203},"().",[115,2038,1153],{"class":132},[115,2040,2041],{"class":203},"(), req.From, req.To, req.Amount); err ",[115,2043,564],{"class":128},[115,2045,379],{"class":210},[115,2047,256],{"class":203},[115,2049,2050,2052,2054,2056,2058,2061],{"class":117,"line":234},[115,2051,376],{"class":128},[115,2053,1977],{"class":203},[115,2055,2002],{"class":132},[115,2057,315],{"class":203},[115,2059,2060],{"class":210},"500",[115,2062,2010],{"class":203},[115,2064,2065],{"class":117,"line":240},[115,2066,398],{"class":203},[115,2068,2069,2071,2073,2076,2078,2081],{"class":117,"line":245},[115,2070,404],{"class":128},[115,2072,1977],{"class":203},[115,2074,2075],{"class":132},"NoContent",[115,2077,315],{"class":203},[115,2079,2080],{"class":210},"204",[115,2082,237],{"class":203},[115,2084,2085],{"class":117,"line":259},[115,2086,298],{"class":203},[11,2088,2090],{"id":2089},"domain-service-vs-application-service","Domain Service vs Application Service",[16,2092,2093],{},"Самая частая путаница. Различие критичное.",[2095,2096,2097,2112],"table",{},[2098,2099,2100],"thead",{},[2101,2102,2103,2106,2109],"tr",{},[2104,2105],"th",{},[2104,2107,2108],{},"Domain Service",[2104,2110,2111],{},"Application Service",[2113,2114,2115,2129,2142,2155,2167,2183],"tbody",{},[2101,2116,2117,2123,2126],{},[2118,2119,2120],"td",{},[20,2121,2122],{},"Зачем",[2118,2124,2125],{},"бизнес-правило между entity",[2118,2127,2128],{},"оркестрация шагов use case",[2101,2130,2131,2136,2139],{},[2118,2132,2133],{},[20,2134,2135],{},"Знает про",[2118,2137,2138],{},"только domain",[2118,2140,2141],{},"repo, event bus, tx",[2101,2143,2144,2149,2152],{},[2118,2145,2146],{},[20,2147,2148],{},"Транзакции",[2118,2150,2151],{},"нет",[2118,2153,2154],{},"да",[2101,2156,2157,2162,2164],{},[2118,2158,2159],{},[20,2160,2161],{},"Внешние вызовы",[2118,2163,2151],{},[2118,2165,2166],{},"да (HTTP, БД)",[2101,2168,2169,2174,2178],{},[2118,2170,2171],{},[20,2172,2173],{},"Имена",[2118,2175,2176],{},[32,2177,1654],{},[2118,2179,2180],{},[32,2181,2182],{},"TransferMoneyHandler.Execute",[2101,2184,2185,2190,2193],{},[2118,2186,2187],{},[20,2188,2189],{},"Тест",[2118,2191,2192],{},"юнит, чистый Go",[2118,2194,2195],{},"юнит с моками портов",[16,2197,2198],{},"Правило: если убрать БД, очереди, HTTP — Domain Service остаётся. Application Service — пропадает.",[16,2200,2201],{},[20,2202,2203],{},"Что уходит в Domain:",[56,2205,2206,2212,2218],{},[59,2207,2208,2209],{},"\"Заказ нельзя отменить после оплаты\" → метод ",[32,2210,2211],{},"Order.Cancel()",[59,2213,2214,2215],{},"\"Скидка считается так-то для VIP-клиента\" → ",[32,2216,2217],{},"DiscountPolicy.Calculate()",[59,2219,2220,2221],{},"\"Перевести деньги между счетами\" → ",[32,2222,2223],{},"TransferService.Transfer()",[16,2225,2226],{},[20,2227,2228],{},"Что уходит в Application:",[56,2230,2231,2237,2243],{},[59,2232,2233,2234],{},"\"При отмене заказа: снять hold с карты, отправить email\" → ",[32,2235,2236],{},"CancelOrderHandler",[59,2238,2239,2240],{},"\"Загрузить заказ из БД, применить скидку, сохранить\" → ",[32,2241,2242],{},"ApplyDiscountHandler",[59,2244,2245,2246],{},"\"По REST-запросу: создать заказ, опубликовать event\" → ",[32,2247,2248],{},"CreateOrderHandler",[11,2250,2252],{"id":2251},"domain-events","Domain Events",[16,2254,2255],{},"Механизм связи между агрегатами без прямой ссылки. Когда что-то значимое произошло в домене — публикуется событие. Подписчики реагируют.",[25,2257,2259],{"className":109,"code":2258,"language":111,"meta":5,"style":5},"\u002F\u002F domain\u002Fevents.go\npackage domain\n\ntype OrderPaid struct {\n    OrderID OrderID\n    Amount  Money\n    PaidAt  time.Time\n}\n",[32,2260,2261,2266,2272,2276,2287,2294,2301,2314],{"__ignoreMap":5},[115,2262,2263],{"class":117,"line":118},[115,2264,2265],{"class":121},"\u002F\u002F domain\u002Fevents.go\n",[115,2267,2268,2270],{"class":117,"line":125},[115,2269,129],{"class":128},[115,2271,133],{"class":132},[115,2273,2274],{"class":117,"line":136},[115,2275,140],{"emptyLinePlaceholder":139},[115,2277,2278,2280,2283,2285],{"class":117,"line":143},[115,2279,167],{"class":128},[115,2281,2282],{"class":132}," OrderPaid",[115,2284,253],{"class":128},[115,2286,256],{"class":203},[115,2288,2289,2292],{"class":117,"line":159},[115,2290,2291],{"class":203},"    OrderID ",[115,2293,265],{"class":132},[115,2295,2296,2299],{"class":117,"line":164},[115,2297,2298],{"class":203},"    Amount  ",[115,2300,292],{"class":132},[115,2302,2303,2306,2309,2311],{"class":117,"line":176},[115,2304,2305],{"class":203},"    PaidAt  ",[115,2307,2308],{"class":132},"time",[115,2310,40],{"class":203},[115,2312,2313],{"class":132},"Time\n",[115,2315,2316],{"class":117,"line":181},[115,2317,298],{"class":203},[16,2319,2320],{},"Entity накапливает события:",[25,2322,2324],{"className":109,"code":2323,"language":111,"meta":5,"style":5},"type Order struct {\n    \u002F\u002F ...\n    events []any\n}\n\nfunc (o *Order) Pay() error {\n    if o.status != StatusDraft {\n        return errors.New(\"only draft can be paid\")\n    }\n    o.status = StatusPaid\n    o.events = append(o.events, OrderPaid{OrderID: o.id, Amount: o.total, PaidAt: time.Now()})\n    return nil\n}\n\nfunc (o *Order) PullEvents() []any {\n    e := o.events\n    o.events = nil\n    return e\n}\n",[32,2325,2326,2336,2341,2349,2353,2357,2379,2389,2404,2408,2416,2441,2447,2451,2455,2480,2490,2498,2505],{"__ignoreMap":5},[115,2327,2328,2330,2332,2334],{"class":117,"line":118},[115,2329,167],{"class":128},[115,2331,250],{"class":132},[115,2333,253],{"class":128},[115,2335,256],{"class":203},[115,2337,2338],{"class":117,"line":125},[115,2339,2340],{"class":121},"    \u002F\u002F ...\n",[115,2342,2343,2346],{"class":117,"line":136},[115,2344,2345],{"class":203},"    events []",[115,2347,2348],{"class":132},"any\n",[115,2350,2351],{"class":117,"line":143},[115,2352,298],{"class":203},[115,2354,2355],{"class":117,"line":159},[115,2356,140],{"emptyLinePlaceholder":139},[115,2358,2359,2361,2363,2365,2367,2369,2371,2373,2375,2377],{"class":117,"line":164},[115,2360,309],{"class":128},[115,2362,445],{"class":203},[115,2364,448],{"class":318},[115,2366,339],{"class":128},[115,2368,342],{"class":132},[115,2370,455],{"class":203},[115,2372,548],{"class":132},[115,2374,461],{"class":203},[115,2376,347],{"class":128},[115,2378,256],{"class":203},[115,2380,2381,2383,2385,2387],{"class":117,"line":176},[115,2382,356],{"class":128},[115,2384,473],{"class":203},[115,2386,564],{"class":128},[115,2388,567],{"class":203},[115,2390,2391,2393,2395,2397,2399,2402],{"class":117,"line":181},[115,2392,376],{"class":128},[115,2394,486],{"class":203},[115,2396,385],{"class":132},[115,2398,315],{"class":203},[115,2400,2401],{"class":149},"\"only draft can be paid\"",[115,2403,237],{"class":203},[115,2405,2406],{"class":117,"line":192},[115,2407,398],{"class":203},[115,2409,2410,2412,2414],{"class":117,"line":197},[115,2411,506],{"class":203},[115,2413,509],{"class":128},[115,2415,598],{"class":203},[115,2417,2418,2421,2423,2426,2429,2432,2435,2438],{"class":117,"line":207},[115,2419,2420],{"class":203},"    o.events ",[115,2422,509],{"class":128},[115,2424,2425],{"class":132}," append",[115,2427,2428],{"class":203},"(o.events, ",[115,2430,2431],{"class":132},"OrderPaid",[115,2433,2434],{"class":203},"{OrderID: o.id, Amount: o.total, PaidAt: time.",[115,2436,2437],{"class":132},"Now",[115,2439,2440],{"class":203},"()})\n",[115,2442,2443,2445],{"class":117,"line":222},[115,2444,404],{"class":128},[115,2446,520],{"class":210},[115,2448,2449],{"class":117,"line":228},[115,2450,298],{"class":203},[115,2452,2453],{"class":117,"line":234},[115,2454,140],{"emptyLinePlaceholder":139},[115,2456,2457,2459,2461,2463,2465,2467,2469,2472,2475,2478],{"class":117,"line":240},[115,2458,309],{"class":128},[115,2460,445],{"class":203},[115,2462,448],{"class":318},[115,2464,339],{"class":128},[115,2466,342],{"class":132},[115,2468,455],{"class":203},[115,2470,2471],{"class":132},"PullEvents",[115,2473,2474],{"class":203},"() []",[115,2476,2477],{"class":132},"any",[115,2479,256],{"class":203},[115,2481,2482,2485,2487],{"class":117,"line":245},[115,2483,2484],{"class":203},"    e ",[115,2486,974],{"class":128},[115,2488,2489],{"class":203}," o.events\n",[115,2491,2492,2494,2496],{"class":117,"line":259},[115,2493,2420],{"class":203},[115,2495,509],{"class":128},[115,2497,520],{"class":210},[115,2499,2500,2502],{"class":117,"line":268},[115,2501,404],{"class":128},[115,2503,2504],{"class":203}," e\n",[115,2506,2507],{"class":117,"line":277},[115,2508,298],{"class":203},[16,2510,2511],{},"Application Service публикует после сохранения:",[25,2513,2515],{"className":109,"code":2514,"language":111,"meta":5,"style":5},"func (h *PayOrderHandler) Execute(ctx context.Context, id OrderID) error {\n    order, _ := h.repo.FindByID(ctx, id)\n    if err := order.Pay(); err != nil {\n        return err\n    }\n    if err := h.repo.Save(ctx, order); err != nil {\n        return err\n    }\n    for _, e := range order.PullEvents() {\n        h.bus.Publish(ctx, e)\n    }\n    return nil\n}\n",[32,2516,2517,2556,2570,2592,2598,2602,2623,2629,2633,2653,2663,2667,2673],{"__ignoreMap":5},[115,2518,2519,2521,2523,2525,2527,2530,2532,2534,2536,2538,2540,2542,2544,2546,2548,2550,2552,2554],{"class":117,"line":118},[115,2520,309],{"class":128},[115,2522,445],{"class":203},[115,2524,1335],{"class":318},[115,2526,339],{"class":128},[115,2528,2529],{"class":132},"PayOrderHandler",[115,2531,455],{"class":203},[115,2533,1345],{"class":132},[115,2535,315],{"class":203},[115,2537,1145],{"class":318},[115,2539,1148],{"class":132},[115,2541,40],{"class":203},[115,2543,1153],{"class":132},[115,2545,324],{"class":203},[115,2547,319],{"class":318},[115,2549,170],{"class":132},[115,2551,455],{"class":203},[115,2553,347],{"class":128},[115,2555,256],{"class":203},[115,2557,2558,2561,2563,2565,2567],{"class":117,"line":125},[115,2559,2560],{"class":203},"    order, _ ",[115,2562,974],{"class":128},[115,2564,1428],{"class":203},[115,2566,1431],{"class":132},[115,2568,2569],{"class":203},"(ctx, id)\n",[115,2571,2572,2574,2576,2578,2581,2583,2586,2588,2590],{"class":117,"line":136},[115,2573,356],{"class":128},[115,2575,971],{"class":203},[115,2577,974],{"class":128},[115,2579,2580],{"class":203}," order.",[115,2582,548],{"class":132},[115,2584,2585],{"class":203},"(); err ",[115,2587,564],{"class":128},[115,2589,379],{"class":210},[115,2591,256],{"class":203},[115,2593,2594,2596],{"class":117,"line":143},[115,2595,376],{"class":128},[115,2597,996],{"class":203},[115,2599,2600],{"class":117,"line":159},[115,2601,398],{"class":203},[115,2603,2604,2606,2608,2610,2612,2614,2617,2619,2621],{"class":117,"line":164},[115,2605,356],{"class":128},[115,2607,971],{"class":203},[115,2609,974],{"class":128},[115,2611,1428],{"class":203},[115,2613,1548],{"class":132},[115,2615,2616],{"class":203},"(ctx, order); err ",[115,2618,564],{"class":128},[115,2620,379],{"class":210},[115,2622,256],{"class":203},[115,2624,2625,2627],{"class":117,"line":176},[115,2626,376],{"class":128},[115,2628,996],{"class":203},[115,2630,2631],{"class":117,"line":181},[115,2632,398],{"class":203},[115,2634,2635,2638,2641,2643,2646,2648,2650],{"class":117,"line":192},[115,2636,2637],{"class":128},"    for",[115,2639,2640],{"class":203}," _, e ",[115,2642,974],{"class":128},[115,2644,2645],{"class":128}," range",[115,2647,2580],{"class":203},[115,2649,2471],{"class":132},[115,2651,2652],{"class":203},"() {\n",[115,2654,2655,2658,2660],{"class":117,"line":197},[115,2656,2657],{"class":203},"        h.bus.",[115,2659,1612],{"class":132},[115,2661,2662],{"class":203},"(ctx, e)\n",[115,2664,2665],{"class":117,"line":207},[115,2666,398],{"class":203},[115,2668,2669,2671],{"class":117,"line":222},[115,2670,404],{"class":128},[115,2672,520],{"class":210},[115,2674,2675],{"class":117,"line":228},[115,2676,298],{"class":203},[16,2678,2679,2680,2682,2683,40],{},"Подписчик в другом контексте слушает ",[32,2681,2431],{}," и стартует доставку, начисляет бонусы, шлёт email — без прямого вызова из ",[32,2684,342],{},[11,2686,2688],{"id":2687},"структура-проекта-screaming-architecture","Структура проекта (Screaming Architecture)",[16,2690,2691,2692,2695],{},"Robert Martin: \"архитектура должна ",[20,2693,2694],{},"кричать"," о домене, а не о фреймворке\". В Onion это работает: верхние папки — про бизнес, не про технологии.",[16,2697,2698],{},"Плохо (кричит про Spring\u002FEcho):",[25,2700,2703],{"className":2701,"code":2702,"language":30,"meta":5},[28],"controllers\u002F\nservices\u002F\nrepositories\u002F\nentities\u002F\n",[32,2704,2702],{"__ignoreMap":5},[16,2706,2707],{},"Хорошо (кричит про OrderService \u002F Billing):",[25,2709,2712],{"className":2710,"code":2711,"language":30,"meta":5},[28],"internal\u002F\n  order\u002F                    ← bounded context\n    domain\u002F\n      order.go\n      events.go\n      transfer_service.go\n    application\u002F\n      create_order.go\n      cancel_order.go\n      pay_order.go\n    infrastructure\u002F\n      postgres\u002F\n        order_repo.go\n      http\u002F\n        order_handler.go\n  billing\u002F                  ← другой context\n    domain\u002F\n    application\u002F\n    infrastructure\u002F\n  cmd\u002F\n    server\u002F\n      main.go\n",[32,2713,2711],{"__ignoreMap":5},[16,2715,2716],{},"Открыл репу — видишь \"Order, Billing, Shipping\". Видишь домен. Не видишь \"controllers, services, dao\".",[11,2718,2720],{"id":2719},"production-границы-в-onion","Production-границы в Onion",[16,2722,2723],{},"Onion легко испортить, если воспринимать слои как названия папок. В продакшене проверяют не красоту дерева, а направление знания:",[56,2725,2726,2729,2732,2735],{},[59,2727,2728],{},"Domain Model знает правила и термины предметной области, но не знает HTTP, SQL, Redis, Kafka, logger и config.",[59,2730,2731],{},"Domain Service содержит правило, которое не принадлежит одной entity, но всё ещё не ходит в сеть и БД.",[59,2733,2734],{},"Application Service оркестрирует сценарий: загружает агрегаты, вызывает domain, сохраняет результат, пишет outbox, управляет транзакцией.",[59,2736,2737],{},"Infrastructure переводит внешний мир в понятные ядру интерфейсы и модели.",[16,2739,2740],{},"Хороший запах: доменный пакет можно собрать и протестировать отдельно, без поднятого окружения.",[16,2742,2743],{},"Плохой запах:",[25,2745,2747],{"className":109,"code":2746,"language":111,"meta":5,"style":5},"package conversion\n\ntype Quote struct {\n    ID        string `db:\"id\" json:\"id\"`\n    TraceID   string `json:\"trace_id\"`\n    CreatedAt time.Time\n}\n\nfunc (q Quote) LogValue() slog.Value { \u002F* ... *\u002F }\n",[32,2748,2749,2756,2760,2771,2782,2792,2803,2807,2811],{"__ignoreMap":5},[115,2750,2751,2753],{"class":117,"line":118},[115,2752,129],{"class":128},[115,2754,2755],{"class":132}," conversion\n",[115,2757,2758],{"class":117,"line":125},[115,2759,140],{"emptyLinePlaceholder":139},[115,2761,2762,2764,2767,2769],{"class":117,"line":136},[115,2763,167],{"class":128},[115,2765,2766],{"class":132}," Quote",[115,2768,253],{"class":128},[115,2770,256],{"class":203},[115,2772,2773,2776,2779],{"class":117,"line":143},[115,2774,2775],{"class":203},"    ID        ",[115,2777,2778],{"class":128},"string",[115,2780,2781],{"class":149}," `db:\"id\" json:\"id\"`\n",[115,2783,2784,2787,2789],{"class":117,"line":159},[115,2785,2786],{"class":203},"    TraceID   ",[115,2788,2778],{"class":128},[115,2790,2791],{"class":149}," `json:\"trace_id\"`\n",[115,2793,2794,2797,2799,2801],{"class":117,"line":164},[115,2795,2796],{"class":203},"    CreatedAt ",[115,2798,2308],{"class":132},[115,2800,40],{"class":203},[115,2802,2313],{"class":132},[115,2804,2805],{"class":117,"line":176},[115,2806,298],{"class":203},[115,2808,2809],{"class":117,"line":181},[115,2810,140],{"emptyLinePlaceholder":139},[115,2812,2813,2815,2817,2820,2823,2825,2828,2830,2833,2835,2838,2841,2844],{"class":117,"line":192},[115,2814,309],{"class":128},[115,2816,445],{"class":203},[115,2818,2819],{"class":318},"q ",[115,2821,2822],{"class":132},"Quote",[115,2824,455],{"class":203},[115,2826,2827],{"class":132},"LogValue",[115,2829,461],{"class":203},[115,2831,2832],{"class":132},"slog",[115,2834,40],{"class":203},[115,2836,2837],{"class":132},"Value",[115,2839,2840],{"class":203}," { ",[115,2842,2843],{"class":121},"\u002F* ... *\u002F",[115,2845,1709],{"class":203},[16,2847,2848],{},"Здесь доменная модель знает о DB tags, JSON contract и logging. Каждый из этих контрактов меняется по другой причине, поэтому им место на границе: DB row в repository adapter, response DTO в transport adapter, log fields в middleware\u002Fusecase boundary.",[16,2850,2851],{},"Практическая проверка для review:",[56,2853,2854,2857,2860,2863],{},[59,2855,2856],{},"можно ли объяснить каждый import из domain как часть предметной области?",[59,2858,2859],{},"можно ли заменить Echo на gRPC без изменения domain?",[59,2861,2862],{},"можно ли заменить Postgres на in-memory fake в use case test?",[59,2864,2865,2866,2869],{},"не лежат ли ",[32,2867,2868],{},"context.Value",", logger или tracer внутри entity как скрытая зависимость?",[11,2871,2873],{"id":2872},"pitfalls","Pitfalls",[16,2875,2876,2879,2880,2883],{},[20,2877,2878],{},"Domain импортирует Infrastructure."," Самая частая ошибка. Entity использует ",[32,2881,2882],{},"gorm.Model"," или маршалит в JSON через теги. Лечится: модели БД отдельные, маппинг на границе.",[16,2885,2886,2889,2890,2893,2894,2897,2898,2901],{},[20,2887,2888],{},"Domain Service вместо метода entity."," Логику \"Order.Cancel()\" вынесли в ",[32,2891,2892],{},"OrderService.CancelOrder(o *Order)",". Анемичная модель. Если правило про ",[20,2895,2896],{},"одну"," entity — клади метод ",[20,2899,2900],{},"в"," entity.",[16,2903,2904,2907,2908,2910],{},[20,2905,2906],{},"Application Service с бизнес-логикой."," В ",[32,2909,2248],{}," 200 строк проверок: VIP-скидка, лимит, валидация. Это правила домена, должны быть в entity или Domain Service.",[16,2912,2913,2916],{},[20,2914,2915],{},"Anaemic Domain Model."," Структуры с публичными полями, без методов, вся логика в сервисах. Это процедурный код в Onion-обёртке.",[16,2918,2919,2922],{},[20,2920,2921],{},"Утечка events наружу."," Domain event сериализуется как DTO для внешнего API. Это два разных события: внутреннее (доменное) и внешнее (контракт). Разделяй.",[16,2924,2925,2928],{},[20,2926,2927],{},"Переусложнение."," На простом CRUD заводят 4 слоя, где Domain Service пуст, Application Service просто зовёт repo. Если домена нет — Onion не нужен. Делай Layered\u002FHexagonal без внутренних колец.",[16,2930,2931,2934],{},[20,2932,2933],{},"Импорты вверх."," Domain импортирует Application, потому что \"удобно\". Это рушит весь смысл — направление зависимостей нарушено.",[16,2936,2937,2940,2941,2943],{},[20,2938,2939],{},"God Aggregate."," Один ",[32,2942,342],{}," тянет всё: items, payment, shipping, customer, history. Делите по транзакционным границам.",[11,2945,2947],{"id":2946},"сравнение-с-альтернативами","Сравнение с альтернативами",[16,2949,2950],{},[20,2951,2952],{},"Без Onion (Hexagonal):",[56,2954,2955,2958,2961],{},[59,2956,2957],{},"Один \"сервис\" на сценарий",[59,2959,2960],{},"Логика и оркестрация перемешаны",[59,2962,2963],{},"Подходит для среднего размера",[16,2965,2966],{},[20,2967,2968],{},"Onion:",[56,2970,2971,2974,2977],{},[59,2972,2973],{},"Чёткое разделение Domain \u002F Application \u002F Infrastructure",[59,2975,2976],{},"Подходит для сложного домена",[59,2978,2979],{},"Структура папок отражает контексты",[16,2981,2982],{},[20,2983,2984],{},"Clean Architecture:",[56,2986,2987,2990,2993],{},[59,2988,2989],{},"То же + явные Use Case-боундари + DTO между слоями",[59,2991,2992],{},"Формализовано Robert Martin",[59,2994,2995],{},"Дополнительный boilerplate ради ещё большей изоляции",[16,2997,2998],{},[20,2999,3000],{},"DDD:",[56,3002,3003,3006],{},[59,3004,3005],{},"Onion часто идёт в комплекте с DDD (агрегаты, ubiquitous language, bounded contexts)",[59,3007,3008],{},"Но Onion работает и без DDD",[11,3010,3012],{"id":3011},"чек-лист-ревью","Чек-лист ревью",[56,3014,3017,3037,3043,3049,3055,3065,3071,3077,3083,3089],{"className":3015},[3016],"contains-task-list",[59,3018,3021,3025,3026,3029,3030,3033,3034],{"className":3019},[3020],"task-list-item",[3022,3023],"input",{"disabled":139,"type":3024},"checkbox"," ",[32,3027,3028],{},"domain\u002F"," не импортирует ничего из ",[32,3031,3032],{},"application\u002F"," или ",[32,3035,3036],{},"infrastructure\u002F",[59,3038,3040,3042],{"className":3039},[3020],[3022,3041],{"disabled":139,"type":3024}," Поля entity приватные, изменения через методы",[59,3044,3046,3048],{"className":3045},[3020],[3022,3047],{"disabled":139,"type":3024}," Инварианты проверяются в методах entity, а не в сервисах",[59,3050,3052,3054],{"className":3051},[3020],[3022,3053],{"disabled":139,"type":3024}," Value objects иммутабельны и валидируют себя в конструкторе",[59,3056,3058,3060,3061,3064],{"className":3057},[3020],[3022,3059],{"disabled":139,"type":3024}," Domain Service используется только когда логика про ",[20,3062,3063],{},"несколько"," entity",[59,3066,3068,3070],{"className":3067},[3020],[3022,3069],{"disabled":139,"type":3024}," Application Service не содержит бизнес-правил (только оркестрация)",[59,3072,3074,3076],{"className":3073},[3020],[3022,3075],{"disabled":139,"type":3024}," Транзакции только в Application слое",[59,3078,3080,3082],{"className":3079},[3020],[3022,3081],{"disabled":139,"type":3024}," Domain events публикуются после сохранения, не во время",[59,3084,3086,3088],{"className":3085},[3020],[3022,3087],{"disabled":139,"type":3024}," Структура папок отражает домен, а не технологию (screaming architecture)",[59,3090,3092,3094],{"className":3091},[3020],[3022,3093],{"disabled":139,"type":3024}," Маппинг DB-моделей в Domain entity на границе (в repo)",[11,3096,3098],{"id":3097},"вопросы-для-интервью","Вопросы для интервью",[16,3100,3101,3104],{},[20,3102,3103],{},"Q: Что такое Onion Architecture?","\nA: Архитектура с концентрическими слоями вокруг Domain Model. Зависимости направлены строго внутрь: Infrastructure → Application → Domain. Автор — Jeffrey Palermo, 2008. Развитие Hexagonal: добавляет явные слои внутри ядра.",[16,3106,3107,3110],{},[20,3108,3109],{},"Q: Сколько слоёв в Onion?","\nA: Обычно 4: Domain Model (entities, value objects), Domain Services (логика между entity), Application Services (use cases, оркестрация), Infrastructure (БД, HTTP, очереди). Можно объединять — но направление зависимостей сохраняется.",[16,3112,3113,3116,3117,3120,3121,3124],{},[20,3114,3115],{},"Q: Чем Domain Service отличается от Application Service?","\nA: Domain Service содержит ",[20,3118,3119],{},"бизнес-логику",", работающую с несколькими entity, без БД и транзакций. Application Service — ",[20,3122,3123],{},"оркестрация сценария",": загрузить из repo, дёрнуть domain, сохранить, опубликовать событие. Если убрать инфраструктуру, Domain Service остаётся, Application — нет.",[16,3126,3127,3130,3131,3134],{},[20,3128,3129],{},"Q: Что такое богатая (rich) и анемичная (anaemic) модель?","\nA: Rich — у entity приватные поля и методы с инвариантами (",[32,3132,3133],{},"order.Cancel()","). Anaemic — публичные поля без методов, логика в сервисах. Onion и DDD требуют rich. Anaemic — антипаттерн Фаулера.",[16,3136,3137,3140,3141,3144,3145,3147],{},[20,3138,3139],{},"Q: Зачем нужны Domain Events?","\nA: Чтобы агрегаты общались между собой без прямых вызовов. ",[32,3142,3143],{},"Order.Pay()"," публикует ",[32,3146,2431],{},", на это подписан Billing-контекст. Связность падает, можно асинхронно, можно для аудита (event sourcing).",[16,3149,3150,3153,3154,324,3157,3160,3161,324,3164,3167],{},[20,3151,3152],{},"Q: Что такое Screaming Architecture?","\nA: Идея Robert Martin: структура папок должна \"кричать\" о домене (",[32,3155,3156],{},"order\u002F",[32,3158,3159],{},"billing\u002F","), а не о фреймворке (",[32,3162,3163],{},"controllers\u002F",[32,3165,3166],{},"services\u002F","). Onion поддерживает это естественно — bounded context на верхнем уровне.",[16,3169,3170,3173,3174,324,3176,324,3178,324,3181,3184,3185,3188,3189,40],{},[20,3171,3172],{},"Q: Можно ли в Domain слое использовать стандартную библиотеку Go?","\nA: Да — ",[32,3175,2308],{},[32,3177,153],{},[32,3179,3180],{},"fmt",[32,3182,3183],{},"strings"," это часть языка. Запрет на внешние пакеты, особенно инфраструктурные (sql, http, kafka, gorm). Стандартная библиотека — серая зона: ",[32,3186,3187],{},"time.Now()"," в домене затруднит тесты, лучше инжектить через интерфейс ",[32,3190,3191],{},"Clock",[16,3193,3194,3197,3198,3201],{},[20,3195,3196],{},"Q: Чем Onion отличается от Hexagonal?","\nA: Hexagonal — про порты и адаптеры (как ядро общается наружу). Onion добавляет структуру ",[20,3199,3200],{},"внутри"," ядра: Domain Model, Domain Services, Application Services. По сути Hexagonal с разделением логики и оркестрации.",[16,3203,3204,3207,3208,3211],{},[20,3205,3206],{},"Q: Где проводить транзакционную границу?","\nA: На уровне Application Service, обычно через UnitOfWork или функцию ",[32,3209,3210],{},"tx.Run(func(ctx))",". Domain не знает о транзакциях. Граница транзакции = граница агрегата.",[16,3213,3214,3217],{},[20,3215,3216],{},"Q: Когда не надо Onion?","\nA: Простой CRUD без бизнес-правил — слои будут пустыми. Прототип — лишний boilerplate. Маленький сервис на 1-2 endpoint. Если нет домена, Onion навредит, не поможет.",[3219,3220],"hr",{},[11,3222,3224],{"id":3223},"практика","Практика",[3226,3227,3231,3234,3251],"quiz",{"answer":3228,"id":3229,"xp":3230},"3","arch-onion-q1","10",[16,3232,3233],{},"Куда должны быть направлены зависимости в Onion Architecture?",[3235,3236,3237],"template",{"v-slot:options":5},[56,3238,3239,3242,3245,3248],{},[59,3240,3241],{},"От domain к infrastructure",[59,3243,3244],{},"От infrastructure к database",[59,3246,3247],{},"От внешних колец к внутренним",[59,3249,3250],{},"В любую сторону, если нет import cycle",[3235,3252,3253],{"v-slot:explanation":5},[16,3254,3255],{},"Onion строится вокруг правила: внутренние кольца не знают о внешних. Infrastructure может знать domain, но domain не должен знать infrastructure.",[3257,3258,3262,3265,3467],"predict",{"answer":3259,"id":3260,"xp":3261},"true\\nfalse","arch-onion-p1","15",[16,3263,3264],{},"Что выведет программа?",[3235,3266,3267],{"v-slot:code":5},[25,3268,3270],{"className":109,"code":3269,"language":111,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc canImport(from, to string) bool {\n    rank := map[string]int{\n        \"domain\": 0,\n        \"application\": 1,\n        \"infrastructure\": 2,\n    }\n    return rank[from] >= rank[to]\n}\n\nfunc main() {\n    fmt.Println(canImport(\"infrastructure\", \"domain\"))\n    fmt.Println(canImport(\"domain\", \"infrastructure\"))\n}\n",[32,3271,3272,3279,3283,3293,3297,3321,3345,3359,3371,3383,3387,3400,3404,3408,3417,3443,3463],{"__ignoreMap":5},[115,3273,3274,3276],{"class":117,"line":118},[115,3275,129],{"class":128},[115,3277,3278],{"class":132}," main\n",[115,3280,3281],{"class":117,"line":125},[115,3282,140],{"emptyLinePlaceholder":139},[115,3284,3285,3287,3289,3291],{"class":117,"line":136},[115,3286,146],{"class":128},[115,3288,150],{"class":149},[115,3290,3180],{"class":132},[115,3292,156],{"class":149},[115,3294,3295],{"class":117,"line":143},[115,3296,140],{"emptyLinePlaceholder":139},[115,3298,3299,3301,3304,3306,3308,3310,3312,3314,3316,3319],{"class":117,"line":159},[115,3300,309],{"class":128},[115,3302,3303],{"class":132}," canImport",[115,3305,315],{"class":203},[115,3307,941],{"class":318},[115,3309,324],{"class":203},[115,3311,946],{"class":318},[115,3313,693],{"class":128},[115,3315,455],{"class":203},[115,3317,3318],{"class":128},"bool",[115,3320,256],{"class":203},[115,3322,3323,3326,3328,3331,3334,3336,3339,3342],{"class":117,"line":164},[115,3324,3325],{"class":203},"    rank ",[115,3327,974],{"class":128},[115,3329,3330],{"class":128}," map",[115,3332,3333],{"class":203},"[",[115,3335,2778],{"class":128},[115,3337,3338],{"class":203},"]",[115,3340,3341],{"class":128},"int",[115,3343,3344],{"class":203},"{\n",[115,3346,3347,3350,3353,3356],{"class":117,"line":176},[115,3348,3349],{"class":149},"        \"domain\"",[115,3351,3352],{"class":203},": ",[115,3354,3355],{"class":210},"0",[115,3357,3358],{"class":203},",\n",[115,3360,3361,3364,3366,3369],{"class":117,"line":181},[115,3362,3363],{"class":149},"        \"application\"",[115,3365,3352],{"class":203},[115,3367,3368],{"class":210},"1",[115,3370,3358],{"class":203},[115,3372,3373,3376,3378,3381],{"class":117,"line":192},[115,3374,3375],{"class":149},"        \"infrastructure\"",[115,3377,3352],{"class":203},[115,3379,3380],{"class":210},"2",[115,3382,3358],{"class":203},[115,3384,3385],{"class":117,"line":197},[115,3386,398],{"class":203},[115,3388,3389,3391,3394,3397],{"class":117,"line":207},[115,3390,404],{"class":128},[115,3392,3393],{"class":203}," rank[from] ",[115,3395,3396],{"class":128},">=",[115,3398,3399],{"class":203}," rank[to]\n",[115,3401,3402],{"class":117,"line":222},[115,3403,298],{"class":203},[115,3405,3406],{"class":117,"line":228},[115,3407,140],{"emptyLinePlaceholder":139},[115,3409,3410,3412,3415],{"class":117,"line":234},[115,3411,309],{"class":128},[115,3413,3414],{"class":132}," main",[115,3416,2652],{"class":203},[115,3418,3419,3422,3425,3427,3430,3432,3435,3437,3440],{"class":117,"line":240},[115,3420,3421],{"class":203},"    fmt.",[115,3423,3424],{"class":132},"Println",[115,3426,315],{"class":203},[115,3428,3429],{"class":132},"canImport",[115,3431,315],{"class":203},[115,3433,3434],{"class":149},"\"infrastructure\"",[115,3436,324],{"class":203},[115,3438,3439],{"class":149},"\"domain\"",[115,3441,3442],{"class":203},"))\n",[115,3444,3445,3447,3449,3451,3453,3455,3457,3459,3461],{"class":117,"line":245},[115,3446,3421],{"class":203},[115,3448,3424],{"class":132},[115,3450,315],{"class":203},[115,3452,3429],{"class":132},[115,3454,315],{"class":203},[115,3456,3439],{"class":149},[115,3458,324],{"class":203},[115,3460,3434],{"class":149},[115,3462,3442],{"class":203},[115,3464,3465],{"class":117,"line":259},[115,3466,298],{"class":203},[3235,3468,3469],{"v-slot:hint":5},[16,3470,3471],{},"Чем ближе к домену, тем меньше число. Импорт разрешён только внутрь.",[3473,3474,3478,3489,3631],"code-task",{"expected":3475,"id":3476,"xp":3477},"ok\\nbad\\nok","arch-onion-ct1","20",[16,3479,3480,3481,3484,3485,3488],{},"Реализуй ",[32,3482,3483],{},"RingDependency",": верни ",[32,3486,3487],{},"\"ok\"",", если зависимость идёт внутрь или остаётся в том же кольце.",[3235,3490,3491],{"v-slot:template":5},[25,3492,3494],{"className":109,"code":3493,"language":111,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc RingDependency(from, to int) string {\n    return \"bad\"\n}\n\nfunc main() {\n    fmt.Println(RingDependency(2, 0))\n    fmt.Println(RingDependency(0, 2))\n    fmt.Println(RingDependency(1, 1))\n}\n",[32,3495,3496,3502,3506,3516,3520,3544,3551,3555,3559,3567,3587,3607,3627],{"__ignoreMap":5},[115,3497,3498,3500],{"class":117,"line":118},[115,3499,129],{"class":128},[115,3501,3278],{"class":132},[115,3503,3504],{"class":117,"line":125},[115,3505,140],{"emptyLinePlaceholder":139},[115,3507,3508,3510,3512,3514],{"class":117,"line":136},[115,3509,146],{"class":128},[115,3511,150],{"class":149},[115,3513,3180],{"class":132},[115,3515,156],{"class":149},[115,3517,3518],{"class":117,"line":143},[115,3519,140],{"emptyLinePlaceholder":139},[115,3521,3522,3524,3527,3529,3531,3533,3535,3538,3540,3542],{"class":117,"line":159},[115,3523,309],{"class":128},[115,3525,3526],{"class":132}," RingDependency",[115,3528,315],{"class":203},[115,3530,941],{"class":318},[115,3532,324],{"class":203},[115,3534,946],{"class":318},[115,3536,3537],{"class":128}," int",[115,3539,455],{"class":203},[115,3541,2778],{"class":128},[115,3543,256],{"class":203},[115,3545,3546,3548],{"class":117,"line":164},[115,3547,404],{"class":128},[115,3549,3550],{"class":149}," \"bad\"\n",[115,3552,3553],{"class":117,"line":176},[115,3554,298],{"class":203},[115,3556,3557],{"class":117,"line":181},[115,3558,140],{"emptyLinePlaceholder":139},[115,3560,3561,3563,3565],{"class":117,"line":192},[115,3562,309],{"class":128},[115,3564,3414],{"class":132},[115,3566,2652],{"class":203},[115,3568,3569,3571,3573,3575,3577,3579,3581,3583,3585],{"class":117,"line":197},[115,3570,3421],{"class":203},[115,3572,3424],{"class":132},[115,3574,315],{"class":203},[115,3576,3483],{"class":132},[115,3578,315],{"class":203},[115,3580,3380],{"class":210},[115,3582,324],{"class":203},[115,3584,3355],{"class":210},[115,3586,3442],{"class":203},[115,3588,3589,3591,3593,3595,3597,3599,3601,3603,3605],{"class":117,"line":207},[115,3590,3421],{"class":203},[115,3592,3424],{"class":132},[115,3594,315],{"class":203},[115,3596,3483],{"class":132},[115,3598,315],{"class":203},[115,3600,3355],{"class":210},[115,3602,324],{"class":203},[115,3604,3380],{"class":210},[115,3606,3442],{"class":203},[115,3608,3609,3611,3613,3615,3617,3619,3621,3623,3625],{"class":117,"line":222},[115,3610,3421],{"class":203},[115,3612,3424],{"class":132},[115,3614,315],{"class":203},[115,3616,3483],{"class":132},[115,3618,315],{"class":203},[115,3620,3368],{"class":210},[115,3622,324],{"class":203},[115,3624,3368],{"class":210},[115,3626,3442],{"class":203},[115,3628,3629],{"class":117,"line":228},[115,3630,298],{"class":203},[3235,3632,3633],{"v-slot:hints":5},[56,3634,3635,3641,3644],{},[59,3636,3637,3638,3640],{},"Считай ",[32,3639,3355],{}," самым внутренним кольцом",[59,3642,3643],{},"Внешнее кольцо может зависеть от внутреннего",[59,3645,3646],{},"Внутреннее кольцо не должно зависеть от внешнего",[3648,3649,3650],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":5,"searchDepth":125,"depth":125,"links":3652},[3653,3654,3655,3661,3662,3663,3664,3665,3666,3667,3668,3669],{"id":13,"depth":125,"text":14},{"id":43,"depth":125,"text":44},{"id":88,"depth":125,"text":89,"children":3656},[3657,3658,3659,3660],{"id":93,"depth":136,"text":94},{"id":852,"depth":136,"text":853},{"id":1069,"depth":136,"text":1070},{"id":1658,"depth":136,"text":1659},{"id":2089,"depth":125,"text":2090},{"id":2251,"depth":125,"text":2252},{"id":2687,"depth":125,"text":2688},{"id":2719,"depth":125,"text":2720},{"id":2872,"depth":125,"text":2873},{"id":2946,"depth":125,"text":2947},{"id":3011,"depth":125,"text":3012},{"id":3097,"depth":125,"text":3098},{"id":3223,"depth":125,"text":3224},1781022065749]