[{"data":1,"prerenderedAt":1566},["ShallowReactive",2],{"navigation_docs_en":3,"-en-database-transactions":342,"-en-database-transactions-surround":1561},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":152,"body":344,"description":1554,"extension":1555,"links":1556,"meta":1557,"navigation":1558,"path":153,"seo":1559,"stem":154,"__hash__":1560},"docs_en\u002Fen\u002F4.database\u002F4.transactions.md",{"type":345,"value":346,"toc":1545},"minimark",[347,368,373,701,730,734,749,764,1233,1251,1294,1313,1317,1327,1366,1372,1382,1386,1399,1435,1442,1446,1470,1474,1523,1527,1541],[348,349,350,351,355,356,360,361,364,365,367],"p",{},"The hard requirement behind ",[352,353,354],"code",{},"dbx",": ",[357,358,359],"strong",{},"a service must be able to call multiple repositories inside one database transaction, without any repository knowing about transactions."," This page covers the pattern in depth: the pgx-specific mechanics live on the ",[362,363,143],"a",{"href":144}," page, the bun variant on the ",[362,366,148],{"href":149}," page.",[369,370,372],"h2",{"id":371},"the-three-pieces","The three pieces",[374,375,380],"pre",{"className":376,"code":377,"language":378,"meta":379,"style":379},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F 1. The contract the service holds (dbx package, driver-independent):\ntype Transactor interface {\n    WithinTransaction(ctx context.Context, fn func(ctx context.Context) error) error\n}\n\n\u002F\u002F 2. The query surface the repositories hold (the pgx variant here):\ntype DBTX interface {\n    Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)\n    Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)\n    QueryRow(ctx context.Context, sql string, args ...any) pgx.Row\n}\n\n\u002F\u002F 3. The executor connecting the two: every call first looks for a\n\u002F\u002F    transaction in the context; when there is none, it runs on the pool.\ndb := pg.NewDB(pool)\ntx := pg.NewTransactor(pool)\n","go","",[352,381,382,391,408,460,466,473,479,491,546,593,633,638,643,649,655,680],{"__ignoreMap":379},[383,384,387],"span",{"class":385,"line":386},"line",1,[383,388,390],{"class":389},"sHwdD","\u002F\u002F 1. The contract the service holds (dbx package, driver-independent):\n",[383,392,394,398,402,405],{"class":385,"line":393},2,[383,395,397],{"class":396},"sMK4o","type",[383,399,401],{"class":400},"sBMFI"," Transactor",[383,403,404],{"class":396}," interface",[383,406,407],{"class":396}," {\n",[383,409,411,415,418,422,425,428,431,434,437,440,442,444,446,448,451,455,457],{"class":385,"line":410},3,[383,412,414],{"class":413},"s2Zo4","    WithinTransaction",[383,416,417],{"class":396},"(",[383,419,421],{"class":420},"sHdIc","ctx",[383,423,424],{"class":400}," context",[383,426,427],{"class":396},".",[383,429,430],{"class":400},"Context",[383,432,433],{"class":396},",",[383,435,436],{"class":420}," fn",[383,438,439],{"class":396}," func(",[383,441,421],{"class":420},[383,443,424],{"class":400},[383,445,427],{"class":396},[383,447,430],{"class":400},[383,449,450],{"class":396},")",[383,452,454],{"class":453},"spNyl"," error",[383,456,450],{"class":396},[383,458,459],{"class":453}," error\n",[383,461,463],{"class":385,"line":462},4,[383,464,465],{"class":396},"}\n",[383,467,469],{"class":385,"line":468},5,[383,470,472],{"emptyLinePlaceholder":471},true,"\n",[383,474,476],{"class":385,"line":475},6,[383,477,478],{"class":389},"\u002F\u002F 2. The query surface the repositories hold (the pgx variant here):\n",[383,480,482,484,487,489],{"class":385,"line":481},7,[383,483,397],{"class":396},[383,485,486],{"class":400}," DBTX",[383,488,404],{"class":396},[383,490,407],{"class":396},[383,492,494,497,499,501,503,505,507,509,512,515,517,520,523,526,528,531,534,536,539,541,543],{"class":385,"line":493},8,[383,495,496],{"class":413},"    Exec",[383,498,417],{"class":396},[383,500,421],{"class":420},[383,502,424],{"class":400},[383,504,427],{"class":396},[383,506,430],{"class":400},[383,508,433],{"class":396},[383,510,511],{"class":420}," sql",[383,513,514],{"class":453}," string",[383,516,433],{"class":396},[383,518,519],{"class":420}," args",[383,521,522],{"class":396}," ...",[383,524,525],{"class":400},"any",[383,527,450],{"class":396},[383,529,530],{"class":396}," (",[383,532,533],{"class":400},"pgconn",[383,535,427],{"class":396},[383,537,538],{"class":400},"CommandTag",[383,540,433],{"class":396},[383,542,454],{"class":453},[383,544,545],{"class":396},")\n",[383,547,549,552,554,556,558,560,562,564,566,568,570,572,574,576,578,580,582,584,587,589,591],{"class":385,"line":548},9,[383,550,551],{"class":413},"    Query",[383,553,417],{"class":396},[383,555,421],{"class":420},[383,557,424],{"class":400},[383,559,427],{"class":396},[383,561,430],{"class":400},[383,563,433],{"class":396},[383,565,511],{"class":420},[383,567,514],{"class":453},[383,569,433],{"class":396},[383,571,519],{"class":420},[383,573,522],{"class":396},[383,575,525],{"class":400},[383,577,450],{"class":396},[383,579,530],{"class":396},[383,581,143],{"class":400},[383,583,427],{"class":396},[383,585,586],{"class":400},"Rows",[383,588,433],{"class":396},[383,590,454],{"class":453},[383,592,545],{"class":396},[383,594,596,599,601,603,605,607,609,611,613,615,617,619,621,623,625,628,630],{"class":385,"line":595},10,[383,597,598],{"class":413},"    QueryRow",[383,600,417],{"class":396},[383,602,421],{"class":420},[383,604,424],{"class":400},[383,606,427],{"class":396},[383,608,430],{"class":400},[383,610,433],{"class":396},[383,612,511],{"class":420},[383,614,514],{"class":453},[383,616,433],{"class":396},[383,618,519],{"class":420},[383,620,522],{"class":396},[383,622,525],{"class":400},[383,624,450],{"class":396},[383,626,627],{"class":400}," pgx",[383,629,427],{"class":396},[383,631,632],{"class":400},"Row\n",[383,634,636],{"class":385,"line":635},11,[383,637,465],{"class":396},[383,639,641],{"class":385,"line":640},12,[383,642,472],{"emptyLinePlaceholder":471},[383,644,646],{"class":385,"line":645},13,[383,647,648],{"class":389},"\u002F\u002F 3. The executor connecting the two: every call first looks for a\n",[383,650,652],{"class":385,"line":651},14,[383,653,654],{"class":389},"\u002F\u002F    transaction in the context; when there is none, it runs on the pool.\n",[383,656,658,662,665,668,670,673,675,678],{"class":385,"line":657},15,[383,659,661],{"class":660},"sTEyZ","db ",[383,663,664],{"class":396},":=",[383,666,667],{"class":660}," pg",[383,669,427],{"class":396},[383,671,672],{"class":413},"NewDB",[383,674,417],{"class":396},[383,676,677],{"class":660},"pool",[383,679,545],{"class":396},[383,681,683,686,688,690,692,695,697,699],{"class":385,"line":682},16,[383,684,685],{"class":660},"tx ",[383,687,664],{"class":396},[383,689,667],{"class":660},[383,691,427],{"class":396},[383,693,694],{"class":413},"NewTransactor",[383,696,417],{"class":396},[383,698,677],{"class":660},[383,700,545],{"class":396},[348,702,703,706,707,710,711,714,715,717,718,721,722,725,726,729],{},[352,704,705],{},"WithinTransaction"," opens a transaction, injects it into the context, and calls ",[352,708,709],{},"fn"," with the ",[357,712,713],{},"modified context",". Inside ",[352,716,709],{},", every repository call passes that same context along. ",[352,719,720],{},"pg.DB"," (or, on bun, the ",[352,723,724],{},"bunx.From","\u002F",[352,727,728],{},"bunx.Conn"," resolvers) fishes the transaction out of it and runs the query on it. Repository signatures do not change; only the executor knows the transaction exists.",[369,731,733],{"id":732},"the-placeorder-service","The PlaceOrder service",[348,735,736,737,740,741,744,745,748],{},"The best example of the pattern is the ",[362,738,739],{"href":41},"shop","'s order placement. Three writes must happen ",[357,742,743],{},"atomically",": inserting the order, decrementing the stock, and the outbox row for the ",[352,746,747],{},"orderPlaced"," event. If any of them fails, none may survive: an order must not exist without its stock deduction, and no confirmation email may go out for an order that was never recorded.",[348,750,751,752,755,756,759,760,763],{},"The full method lives in the module's shared ",[352,753,754],{},"core\u002F"," package (",[352,757,758],{},"internal\u002Fmodules\u002Fshop\u002Fcore\u002F","), called from the ",[352,761,762],{},"api"," surface's service seam:",[374,765,767],{"className":376,"code":766,"language":378,"meta":379,"style":379},"type OrderService struct {\n    orders     *repository.OrderRepository\n    products   *repository.ProductRepository\n    tx         dbx.Transactor\n    dispatcher events.Dispatcher\n}\n\nfunc (s *OrderService) PlaceOrder(ctx context.Context, in PlaceOrderInput) (*Order, error) {\n    var order *Order\n    err := s.tx.WithinTransaction(ctx, func(ctx context.Context) error {\n        var err error\n        order, err = s.orders.Insert(ctx, in)                     \u002F\u002F 1. order\n        if err != nil {\n            return err\n        }\n        if err := s.products.DecrementStock(ctx, in.ProductID, in.Qty); err != nil {\n            return err                                            \u002F\u002F 2. stock\n        }\n        return s.dispatcher.Dispatch(ctx, shopevents.OrderPlaced{ \u002F\u002F 3. outbox row\n            OrderID: order.ID,\n            Email:   in.Email,\n        })\n    })\n    if err != nil {\n        return nil, err\n    }\n    return order, nil\n}\n",[352,768,769,781,797,811,823,836,840,844,898,911,952,963,1000,1016,1024,1029,1082,1093,1098,1136,1156,1173,1179,1185,1199,1209,1215,1228],{"__ignoreMap":379},[383,770,771,773,776,779],{"class":385,"line":386},[383,772,397],{"class":396},[383,774,775],{"class":400}," OrderService",[383,777,778],{"class":396}," struct",[383,780,407],{"class":396},[383,782,783,786,789,792,794],{"class":385,"line":393},[383,784,785],{"class":660},"    orders     ",[383,787,788],{"class":396},"*",[383,790,791],{"class":400},"repository",[383,793,427],{"class":396},[383,795,796],{"class":400},"OrderRepository\n",[383,798,799,802,804,806,808],{"class":385,"line":410},[383,800,801],{"class":660},"    products   ",[383,803,788],{"class":396},[383,805,791],{"class":400},[383,807,427],{"class":396},[383,809,810],{"class":400},"ProductRepository\n",[383,812,813,816,818,820],{"class":385,"line":462},[383,814,815],{"class":660},"    tx         ",[383,817,354],{"class":400},[383,819,427],{"class":396},[383,821,822],{"class":400},"Transactor\n",[383,824,825,828,831,833],{"class":385,"line":468},[383,826,827],{"class":660},"    dispatcher ",[383,829,830],{"class":400},"events",[383,832,427],{"class":396},[383,834,835],{"class":400},"Dispatcher\n",[383,837,838],{"class":385,"line":475},[383,839,465],{"class":396},[383,841,842],{"class":385,"line":481},[383,843,472],{"emptyLinePlaceholder":471},[383,845,846,849,851,854,856,859,861,864,866,868,870,872,874,876,879,882,884,887,890,892,894,896],{"class":385,"line":493},[383,847,848],{"class":396},"func",[383,850,530],{"class":396},[383,852,853],{"class":420},"s ",[383,855,788],{"class":396},[383,857,858],{"class":400},"OrderService",[383,860,450],{"class":396},[383,862,863],{"class":413}," PlaceOrder",[383,865,417],{"class":396},[383,867,421],{"class":420},[383,869,424],{"class":400},[383,871,427],{"class":396},[383,873,430],{"class":400},[383,875,433],{"class":396},[383,877,878],{"class":420}," in",[383,880,881],{"class":400}," PlaceOrderInput",[383,883,450],{"class":396},[383,885,886],{"class":396}," (*",[383,888,889],{"class":400},"Order",[383,891,433],{"class":396},[383,893,454],{"class":453},[383,895,450],{"class":396},[383,897,407],{"class":396},[383,899,900,903,906,908],{"class":385,"line":548},[383,901,902],{"class":396},"    var",[383,904,905],{"class":660}," order ",[383,907,788],{"class":396},[383,909,910],{"class":400},"Order\n",[383,912,913,916,918,921,923,926,928,930,932,934,936,938,940,942,944,946,948,950],{"class":385,"line":595},[383,914,915],{"class":660},"    err ",[383,917,664],{"class":396},[383,919,920],{"class":660}," s",[383,922,427],{"class":396},[383,924,925],{"class":660},"tx",[383,927,427],{"class":396},[383,929,705],{"class":413},[383,931,417],{"class":396},[383,933,421],{"class":660},[383,935,433],{"class":396},[383,937,439],{"class":396},[383,939,421],{"class":420},[383,941,424],{"class":400},[383,943,427],{"class":396},[383,945,430],{"class":400},[383,947,450],{"class":396},[383,949,454],{"class":453},[383,951,407],{"class":396},[383,953,954,957,960],{"class":385,"line":635},[383,955,956],{"class":396},"        var",[383,958,959],{"class":660}," err ",[383,961,962],{"class":453},"error\n",[383,964,965,968,970,972,975,977,979,982,984,987,989,991,993,995,997],{"class":385,"line":640},[383,966,967],{"class":660},"        order",[383,969,433],{"class":396},[383,971,959],{"class":660},[383,973,974],{"class":396},"=",[383,976,920],{"class":660},[383,978,427],{"class":396},[383,980,981],{"class":660},"orders",[383,983,427],{"class":396},[383,985,986],{"class":413},"Insert",[383,988,417],{"class":396},[383,990,421],{"class":660},[383,992,433],{"class":396},[383,994,878],{"class":660},[383,996,450],{"class":396},[383,998,999],{"class":389},"                     \u002F\u002F 1. order\n",[383,1001,1002,1006,1008,1011,1014],{"class":385,"line":645},[383,1003,1005],{"class":1004},"s7zQu","        if",[383,1007,959],{"class":660},[383,1009,1010],{"class":396},"!=",[383,1012,1013],{"class":396}," nil",[383,1015,407],{"class":396},[383,1017,1018,1021],{"class":385,"line":651},[383,1019,1020],{"class":1004},"            return",[383,1022,1023],{"class":660}," err\n",[383,1025,1026],{"class":385,"line":657},[383,1027,1028],{"class":396},"        }\n",[383,1030,1031,1033,1035,1037,1039,1041,1044,1046,1049,1051,1053,1055,1057,1059,1062,1064,1066,1068,1071,1074,1076,1078,1080],{"class":385,"line":682},[383,1032,1005],{"class":1004},[383,1034,959],{"class":660},[383,1036,664],{"class":396},[383,1038,920],{"class":660},[383,1040,427],{"class":396},[383,1042,1043],{"class":660},"products",[383,1045,427],{"class":396},[383,1047,1048],{"class":413},"DecrementStock",[383,1050,417],{"class":396},[383,1052,421],{"class":660},[383,1054,433],{"class":396},[383,1056,878],{"class":660},[383,1058,427],{"class":396},[383,1060,1061],{"class":660},"ProductID",[383,1063,433],{"class":396},[383,1065,878],{"class":660},[383,1067,427],{"class":396},[383,1069,1070],{"class":660},"Qty",[383,1072,1073],{"class":396},");",[383,1075,959],{"class":660},[383,1077,1010],{"class":396},[383,1079,1013],{"class":396},[383,1081,407],{"class":396},[383,1083,1085,1087,1090],{"class":385,"line":1084},17,[383,1086,1020],{"class":1004},[383,1088,1089],{"class":660}," err                                            ",[383,1091,1092],{"class":389},"\u002F\u002F 2. stock\n",[383,1094,1096],{"class":385,"line":1095},18,[383,1097,1028],{"class":396},[383,1099,1101,1104,1106,1108,1111,1113,1116,1118,1120,1122,1125,1127,1130,1133],{"class":385,"line":1100},19,[383,1102,1103],{"class":1004},"        return",[383,1105,920],{"class":660},[383,1107,427],{"class":396},[383,1109,1110],{"class":660},"dispatcher",[383,1112,427],{"class":396},[383,1114,1115],{"class":413},"Dispatch",[383,1117,417],{"class":396},[383,1119,421],{"class":660},[383,1121,433],{"class":396},[383,1123,1124],{"class":400}," shopevents",[383,1126,427],{"class":396},[383,1128,1129],{"class":400},"OrderPlaced",[383,1131,1132],{"class":396},"{",[383,1134,1135],{"class":389}," \u002F\u002F 3. outbox row\n",[383,1137,1139,1142,1145,1148,1150,1153],{"class":385,"line":1138},20,[383,1140,1141],{"class":660},"            OrderID",[383,1143,1144],{"class":396},":",[383,1146,1147],{"class":660}," order",[383,1149,427],{"class":396},[383,1151,1152],{"class":660},"ID",[383,1154,1155],{"class":396},",\n",[383,1157,1159,1162,1164,1167,1169,1171],{"class":385,"line":1158},21,[383,1160,1161],{"class":660},"            Email",[383,1163,1144],{"class":396},[383,1165,1166],{"class":660},"   in",[383,1168,427],{"class":396},[383,1170,252],{"class":660},[383,1172,1155],{"class":396},[383,1174,1176],{"class":385,"line":1175},22,[383,1177,1178],{"class":396},"        })\n",[383,1180,1182],{"class":385,"line":1181},23,[383,1183,1184],{"class":396},"    })\n",[383,1186,1188,1191,1193,1195,1197],{"class":385,"line":1187},24,[383,1189,1190],{"class":1004},"    if",[383,1192,959],{"class":660},[383,1194,1010],{"class":396},[383,1196,1013],{"class":396},[383,1198,407],{"class":396},[383,1200,1202,1204,1207],{"class":385,"line":1201},25,[383,1203,1103],{"class":1004},[383,1205,1206],{"class":396}," nil,",[383,1208,1023],{"class":660},[383,1210,1212],{"class":385,"line":1211},26,[383,1213,1214],{"class":396},"    }\n",[383,1216,1218,1221,1223,1225],{"class":385,"line":1217},27,[383,1219,1220],{"class":1004},"    return",[383,1222,1147],{"class":660},[383,1224,433],{"class":396},[383,1226,1227],{"class":396}," nil\n",[383,1229,1231],{"class":385,"line":1230},28,[383,1232,465],{"class":396},[348,1234,417,1235,1238,1239,1242,1243,1246,1247,1250],{},[352,1236,1237],{},"shopevents"," is the alias of the module's own events package: ",[352,1240,1241],{},"internal\u002Fmodules\u002Fshop\u002Fevents",", generated by ",[352,1244,1245],{},"add event",".) All three calls resolve the same ",[352,1248,1249],{},"pgx.Tx"," from the context:",[1252,1253,1254,1271],"ul",{},[1255,1256,1257,1260,1261,1264,1265,1270],"li",{},[352,1258,1259],{},"orders.Insert"," and ",[352,1262,1263],{},"products.DecrementStock"," through ",[362,1266,1268],{"href":1267},"\u002Fen\u002Fdatabase\u002Fpgx#db--the-executor-that-watches-the-context",[352,1269,720],{},";",[1255,1272,1273,1274,1276,1277,1282,1283,1285,1286,1289,1290,1293],{},"the ",[352,1275,1110],{}," is an ",[362,1278,1279],{"href":183},[352,1280,1281],{},"outbox.NewDispatcher"," whose store also writes through ",[352,1284,720],{},". The event lands in the ",[352,1287,1288],{},"outbox_events"," table ",[357,1291,1292],{},"in the same transaction"," as the business writes. On commit they become visible together; on rollback the event never existed. That is the essence of the transactional outbox.",[348,1295,1296,1297,1299,1300,1303,1304,1306,1307,1312],{},"If ",[352,1298,1048],{}," returns an ",[352,1301,1302],{},"insufficient_stock"," error, the already-executed ",[352,1305,986],{}," rolls back with it, and the event is never delivered. The error maps to a problem+json response through the ",[362,1308,1309],{"href":79},[352,1310,1311],{},"errs"," chain.",[369,1314,1316],{"id":1315},"semantics","Semantics",[348,1318,1319,1320,1260,1323,1326],{},"The guarantees, pinned down by the kit's integration tests (",[352,1321,1322],{},"go test -tags=integration .\u002Fdbx\u002Fpg\u002F",[352,1324,1325],{},".\u002Fdbx\u002Fbunx\u002F",", against a real PostgreSQL):",[1252,1328,1329,1341,1347,1357],{},[1255,1330,1331,1340],{},[357,1332,1333,1334,1336,1337,427],{},"Commit only when ",[352,1335,709],{}," returns ",[352,1338,1339],{},"nil"," Any error rolls back, and propagates to the caller unchanged.",[1255,1342,1343,1346],{},[357,1344,1345],{},"A panic rolls back and is re-raised."," A handler panic cannot leave a transaction dangling.",[1255,1348,1349,1352,1353,1356],{},[357,1350,1351],{},"Rollback survives a cancelled context:"," the cleanup runs with ",[352,1354,1355],{},"context.WithoutCancel",", so a request timeout does not prevent an orderly rollback.",[1255,1358,1359,1365],{},[357,1360,1361,1362,1364],{},"A nested ",[352,1363,705],{}," joins"," the outer transaction: flat nesting, no savepoints. The outermost call always owns commit\u002Frollback: if the inner block succeeds but the outer one fails afterwards, the inner writes roll back too.",[348,1367,1368,1369,1371],{},"The practical consequence of flat nesting: a service method can freely call another service method that itself uses ",[352,1370,705],{},", the inner call becomes part of the outer transaction without noticing. There is no \"are we already in a transaction?\" logic to write.",[1373,1374,1375,1376,1379,1380,427],"warning",{},"There are no savepoints: an error in the inner block rolls back the ",[357,1377,1378],{},"entire"," transaction, not just the inner writes. If you need partial rollback, that is a separate transaction: run it outside the ",[352,1381,705],{},[369,1383,1385],{"id":1384},"why-the-context-and-not-a-parameter","Why the context, and not a parameter?",[348,1387,1388,1389,1391,1392,1394,1395,1398],{},"The classic alternative is passing the transaction by hand: every repository method takes a ",[352,1390,925],{}," parameter, or exists in two variants (",[352,1393,986],{}," \u002F ",[352,1396,1397],{},"InsertTx","). That hurts in three places:",[1400,1401,1402,1416,1425],"ol",{},[1255,1403,1404,1407,1408,1411,1412,1415],{},[357,1405,1406],{},"Every signature gets infected."," ",[352,1409,1410],{},"ListProducts(ctx, params)"," becomes ",[352,1413,1414],{},"ListProducts(ctx, tx, params)",", even for calls that will never have a transaction.",[1255,1417,1418,1421,1422,1424],{},[357,1419,1420],{},"The layers collapse into each other."," The service would have to import and pass around a driver type (",[352,1423,1249],{},"), even though the service layer's job is business logic, not connection management.",[1255,1426,1427,1430,1431,1434],{},[357,1428,1429],{},"Cross-cutting code is left out."," The ",[362,1432,1433],{"href":183},"outbox dispatcher"," is kit code: it cannot know about your repositories' transaction parameter. From the context, though, it reads the transaction exactly like your own repos do.",[348,1436,1437,1438,1441],{},"Context carrying is precisely the kind of implicit state ",[352,1439,1440],{},"context.Context"," is for: request-scoped, travelling down the call chain, cross-cutting. The Go standard library carries traces, deadlines and the cancel signal the same way.",[369,1443,1445],{"id":1444},"implementation-independence","Implementation independence",[348,1447,1448,1449,1452,1453,1456,1457,1460,1461,1464,1465,1469],{},"Not a single line of the service code above is pgx-specific: it depends on ",[352,1450,1451],{},"dbx.Transactor"," and your own repository interfaces. In a ",[352,1454,1455],{},"--db bun"," project the same method compiles and runs unchanged. ",[352,1458,1459],{},"main.go"," injects ",[352,1462,1463],{},"bunx.NewTransactor(bunDB)",", and the repositories' internals use the ",[362,1466,1468],{"href":1467},"\u002Fen\u002Fdatabase\u002Fbun#repositories","bun resolvers",". Drawing the transaction boundary belongs to the service layer; what carries the transaction underneath is a wiring detail.",[369,1471,1473],{"id":1472},"one-transactor-per-connection","One transactor per connection",[348,1475,1476,1477,1480,1481,1483,1484,1488,1489,530,1491,1494,1495,1498,1499,1502,1503,1506,1507,1509,1510,1513,1514,1516,1517,1520,1521,427],{},"A ",[352,1478,1479],{},"Transactor"," is scoped to a single connection: ",[352,1482,705],{}," begins a transaction on the pool it wraps and injects it into the context under that implementation's own key. Repositories bound to a ",[362,1485,1487],{"href":1486},"\u002Fen\u002Fdatabase\u002Foverview#named-connections","named connection"," receive a different ",[352,1490,1479],{},[352,1492,1493],{},"\u003CName>Transactor"," on ",[352,1496,1497],{},"Dependencies",") than the default one: calling the default ",[352,1500,1501],{},"Transactor.WithinTransaction"," around a call to a named connection's repository does ",[357,1504,1505],{},"not"," put that call in the same transaction; it runs on its own connection, uncoordinated. This is also why a connection's connector (pgx or bun) cannot vary per call: a ",[352,1508,1249],{}," and a ",[352,1511,1512],{},"bun.Tx"," are two different types carried under two different context keys, so mixing them on one connection would defeat the whole point of ",[352,1515,1479],{},". If two repositories must commit or roll back together, they must sit behind the ",[357,1518,1519],{},"same"," connection's ",[352,1522,1479],{},[369,1524,1526],{"id":1525},"patterns-used","Patterns used",[348,1528,1529,1530,1533,1534,1537,1538,1540],{},"This page is the full write-up of the ",[362,1531,92],{"href":1532},"\u002Fen\u002Fconcepts\u002Fdesign-patterns#unit-of-work--transaction-in-context"," catalog's ",[357,1535,1536],{},"Unit of Work \u002F transaction in context"," entry: the ",[352,1539,1451],{}," interface and the panic-safe rollback logic, with code, live there.",[1542,1543,1544],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":379,"searchDepth":393,"depth":393,"links":1546},[1547,1548,1549,1550,1551,1552,1553],{"id":371,"depth":393,"text":372},{"id":732,"depth":393,"text":733},{"id":1315,"depth":393,"text":1316},{"id":1384,"depth":393,"text":1385},{"id":1444,"depth":393,"text":1445},{"id":1472,"depth":393,"text":1473},{"id":1525,"depth":393,"text":1526},"Cross-repository transactions carried by the context, no plumbing.","md",null,{},{"icon":155},{"title":152,"description":1554},"GIvaauwid41E01e1PCTNK24TR9CHonJ9iSl1l17r13w",[1562,1564],{"title":148,"path":149,"stem":150,"description":1563,"icon":71,"children":-1},"The optional bun adapter: query builder and struct models, with the Transactor contract unchanged.",{"title":157,"path":158,"stem":159,"description":1565,"icon":160,"children":-1},"Timestamp-prefixed SQL files embedded into the binary, with a generated cmd\u002Fmigrate runner, no external tool.",1784668701157]