Բոլոր դասերը
BackendՄիջին13 րոպե ընթերցումԻմ ստեկում

Նախագծի ճարտարապետություն

Ինչպես կազմակերպել բեքենդի կոդը, որ այն դիմանա աճին, նոր հաճախորդներին և նոր ծրագրավորողներին։

  • Layers
  • Services
  • DTOs
  • Modules
  • Monorepos
Քայլ 1/5
Request է գալիս՝ POST /orders։ Router-ը կարդում է URL-ն ու method-ը և ընտրում controller-ի այն action-ը, որը կմշակի այն։

30 վայրկյանում

Ճարտարապետությունը կոդը հստակ պարտականություններ ունեցող մասերի բաժանելու ձևն է՝ ինչն է մշակում HTTP-ն, որտեղ են ապրում բիզնես կանոնները, ինչպես են պահվում տվյալները։ Լավ կառուցվածքը թույլ է տալիս ավելացնել մոբայլ հավելված, ադմին վահանակ կամ թիմի նոր անդամ՝ առանց ամեն ինչ վերագրելու։ Հիմնական գաղափարներն են՝ շերտերը, սերվիսները, DTO-ները, վալիդացիան եզրերին և մոդուլներն ըստ դոմենի։

Ինչու եմ օգտագործում

  • Ինձ համար ճարտարապետությունը կարևոր է, որովհետև մեկ բեքենդը հաճախ սպասարկում է կայքը, մոբայլ հավելվածը և ադմին վահանակը, և նրանք պետք է կիսեն կանոնները՝ առանց կիսելու յուրաքանչյուր endpoint։
  • Կոդը կառուցում եմ ըստ դոմենի, որպեսզի loyalty-ի կամ inventory-ի նման ֆունկցիոնալը կարողանա աճել՝ առանց մնացած ամեն ինչին դիպչելու։
  • Օգտագործում եմ մոնոռեպոներ, որովհետև API-ի, վեբի և հավելվածների միջև ընդհանուր տիպերն ու փաթեթները վերացնում են ինտեգրացիոն բագերի մի ամբողջ դաս։
  • Հստակ շերտերը code review-ն ու AI-ի օգնությամբ աշխատանքն ավելի անվտանգ են դարձնում՝ բոլորը գիտեն, թե որտեղ է պատկանում փոփոխությունը։

Ինչպես է աշխատում

01Շերտեր՝ յուրաքանչյուր մաս ունի մեկ գործ

Պատկերացրեք ռեստորան։ Մատուցողն ընդունում է պատվերը (HTTP շերտ), խոհարարը որոշում է՝ ինչպես պատրաստել (բիզնես շերտ), պահեստը պահում է մթերքները (տվյալների շերտ)։ Մատուցողը երբեք չի եփում, իսկ խոհարարը երբեք չի խոսում հաճախորդների հետ։ Կոդում՝ կոնտրոլլերները կամ route handler-ները վերլուծում են հարցումը և վերադարձնում պատասխան, սերվիսները կամ use case-երը կիրառում են բիզնես կանոնները, ռեպոզիտորիաները կամ ORM մոդելները կարդում և գրում են տվյալները։ Կախվածություններն ուղղված են դեպի ներս՝ կոնտրոլլերը գիտի սերվիսի մասին, սերվիսը՝ տվյալների շերտի, բայց բիզնես տրամաբանությունը ոչինչ չգիտի HTTP-ի մասին։ Դա նույն տրամաբանությունը դարձնում է վերաօգտագործելի API-ից, CLI հրամանից, queue job-ից կամ թեստից։

  • Ներկայացում (presentation)՝ կոնտրոլլերներ, հարցման վալիդացիա, պատասխանի ձևավորում։
  • Հավելված (application)՝ սերվիսներ կամ use case-եր, որոնք համակարգում են առաջադրանքը (PlaceOrder, RefundPayment)։
  • Դոմեն՝ էնտիտիներ, value object-ներ և կանոններ (Money, OrderStatus-ի անցումներ)։
  • Ենթակառուցվածք (infrastructure)՝ տվյալների բազա, հերթեր, վճարային gateway-ներ, email պրովայդերներ։

02Սերվիսներ, use case-եր և DTO-ներ

Սերվիսը խմբավորում է մեկ դոմենի հետ կապված գործողությունները (OrderService)։ Use case-ը, կամ action-ը, մեկ գործողություն է հստակ անունով (PlaceOrder)։ Երկուսն էլ ընդունում են պարզ մուտքային տվյալներ և վերադարձնում պարզ արդյունք։ Այդ մուտքը DTO է՝ Data Transfer Object, փոքր, տիպավորված օբյեկտ, որը ճշգրիտ նկարագրում է այն դաշտերը, որոնք գործողությունն ընդունում է։ DTO-ները թույլ չեն տալիս հարցման «հում» զանգվածներին թափանցել բիզնես տրամաբանության մեջ և պայմանագիրը դարձնում են ակնհայտ։ Վալիդացրեք եզրերին՝ ստուգեք կառուցվածքն ու տիպերը այնտեղ, որտեղ տվյալները մտնում են համակարգ (HTTP հարցում, webhook, CSV import, հաղորդագրություն հերթից), իսկ ներսում արդեն վստահեք DTO-ին։ Բիզնես կանոնները, օրինակ՝ «չեղարկված պատվերը չի կարող վճարվել», միևնույն է ապրում են դոմենում, որովհետև դրանք մուտքի կառուցվածքի մասին չեն։

ts
// Edge: validate and map to a DTO
export const CreateOrderSchema = z.object({
  branchId: z.string().uuid(),
  items: z.array(z.object({ productId: z.string().uuid(), qty: z.number().int().min(1) })).min(1),
});
export type CreateOrderDto = z.infer<typeof CreateOrderSchema>;
 
// Use case: business logic only, no HTTP
export class PlaceOrder {
  constructor(private orders: OrderRepository, private pricing: PricingService) {}
 
  async execute(companyId: string, dto: CreateOrderDto) {
    const quote = await this.pricing.quote(companyId, dto.items);
    return this.orders.create({ companyId, branchId: dto.branchId, ...quote });
  }
}

03Թղթապանակների կառուցվածք՝ ըստ դոմենի, ոչ թե ըստ ֆայլի տիպի

Փոքր հավելվածները կարող են խմբավորել ըստ տիպի՝ բոլոր կոնտրոլլերները մեկ թղթապանակում, բոլոր մոդելները՝ մյուսում։ Մոտ քսան ֆիչերից հետո դա ցավոտ է դառնում, որովհետև մեկ փոփոխությունը դիպչում է իրարից հեռու հինգ թղթապանակի։ Ըստ դոմենի խմբավորումը, որը երբեմն կոչվում է feature folders կամ մոդուլներ, պատվերների մասին ամեն ինչ պահում է միասին։ Յուրաքանչյուր մոդուլ բացում է փոքր հանրային մակերես (սերվիս, մի քանի տիպ) և թաքցնում մնացածը։ Ընդհանուր, cross-cutting կոդը (auth, լոգավորում, գումարի հետ աշխատող helper-ներ) գնում է shared կամ core թղթապանակ։ Laravel-ում կարող եք պահել ֆրեյմվորքի թղթապանակները և Models-ի ու Services-ի ներսում ավելացնել դոմենային ենթաթղթապանակներ, NestJS-ում մոդուլներն այս մոտեցմանը բնականորեն համապատասխանում են։

bash
src/
  modules/
    orders/
      orders.controller.ts
      orders.service.ts
      dto/create-order.dto.ts
      orders.module.ts
    menu/
    loyalty/
  shared/
    auth/  logging/  money/
  config/
    env.ts

04Առանձին API-ներ վեբի, մոբայլի և ադմինի համար

Մեկ բեքենդը հաճախ ունի երեք լսարան։ Հանրային կայքին, մոբայլ հավելվածին և ադմին վահանակին պետք են պատասխանի տարբեր կառուցվածքներ, տարբեր auth (cookie սեսիա, թոքեն կամ աշխատակցի մուտք 2FA-ով), տարբեր rate limit-ներ և տարբեր թողարկման ցիկլեր՝ մոբայլ հավելվածի տարբերակը կարող է մեկ տարի ապրել հեռախոսներում, ուստի նրա API-ն պետք է մնա հետադարձ համատեղելի։ Լավ պատերն է յուրաքանչյուր ալիքի համար առանձին route խմբեր կամ կոնտրոլլերներ, որոնք կանչում են նույն սերվիսները։ Հաճախորդի այն ֆունկցիաները, որոնք վեբի և մոբայլի համար նույնական են, կարելի է սահմանել մեկ անգամ և միացնել երկու prefix-ների տակ։ Ադմինի endpoint-ներն ապրում են իրենց սեփական խմբում՝ ավելի խիստ թույլտվություններով և audit լոգավորմամբ։ Մոբայլ API-ն տարբերակավորեք բացահայտ (v1, v2), որպեսզի կարողանաք այն անվտանգ զարգացնել։

05Մոնոռեպոներ, կոնֆիգուրացիա և միջավայր

Մոնոռեպոն մի քանի հավելված և փաթեթ պահում է մեկ ռեպոզիտորիայում՝ api, web, admin, mobile, գումարած ընդհանուր փաթեթներ, ինչպիսիք են տիպերը, API client-ը, UI կոմպոնենտները կամ ESLint կոնֆիգը։ pnpm workspaces-ը փաթեթները լոկալ կապում է իրար, իսկ Turborepo-ն (կամ Nx-ը) առաջադրանքները գործարկում է կախվածությունների հերթականությամբ և քեշավորմամբ, այնպես որ վերակառուցվում և թեստավորվում է միայն փոփոխվածը։ Կոնֆիգուրացիան պետք է հետևի twelve-factor կանոնին՝ կոդը ամենուր նույնն է, տարբերվում է միջավայրը։ Միջավայրի փոփոխականները կարդացեք մեկ անգամ՝ գործարկման պահին, վալիդացրեք սխեմայով և անմիջապես ձախողվեք (fail fast), եթե ինչ-որ բան բացակայում է։ Երբեք commit մի արեք .env ֆայլեր, փոխարենը commit արեք .env.example՝ placeholder արժեքներով։

yaml
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
 
# turbo.json
{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] },
    "test": { "dependsOn": ["^build"] },
    "lint": {}
  }
}

06Ինչպես ճարտարապետությունը առողջ պահել ժամանակի ընթացքում

Ճարտարապետությունը քայքայվում է մեկ «կարճ ճանապարհով» ամեն անգամ։ Պաշտպանեք այն կանոններով, որոնք ստուգում են գործիքները՝ lint կանոններ կամ dependency-cruiser, որոնք արգելում են մի մոդուլին ներմուծել մյուսի ներքին մասերը, TypeScript strict ռեժիմ և թեստեր սերվիսների մակարդակում։ Երբ մեծ ընտրություն եք կատարում, գրեք ճարտարապետական կարճ նշումներ (ADR՝ Architecture Decision Record), որպեսզի հաջորդ մարդը իմանա՝ ինչու։ Սկսեք որպես մոդուլային մոնոլիթ՝ մեկ deploy արվող հավելված մոդուլների մաքուր սահմաններով։ Հետագայում սերվիսների բաժանելը հեշտ է, եթե սահմանները մաքուր են, և գրեթե անհնար, եթե՝ ոչ։ Ռեֆակտորինգ արեք անընդհատ, փոքր քայլերով, այլ ոչ թե պլանավորեք մեկ մեծ վերագրում։

Օգտագործեք, երբ

  • Ցանկացած պրոդուկտ, որը սպասվում է, որ կապրի մի քանի ամսից ավելի կամ կաճի մեկ ծրագրավորողից այն կողմ։
  • Բեքենդ, որը սպասարկում է մի քանի client՝ կայք, մոբայլ հավելված, ադմին, գործընկերներ։
  • Մի քանի հավելված, որոնք կիսում են տիպեր, UI կամ տրամաբանություն, որտեղ մոնոռեպոն վերացնում է կրկնությունը։
  • Գոյություն ունեցող կոդաբազային մեծ նոր դոմեն (վճարումներ, inventory) ավելացնելուց առաջ։

Մի՛ օգտագործեք, երբ

  • Մեկանգամյա պրոտոտիպեր և spike-եր, որտեղ սովորելու արագությունն ավելի կարևոր է, քան կառուցվածքը։
  • Ծանր աբստրակցիոն շերտեր ավելացնել «ամեն դեպքում» (ռեպոզիտորիաներ ORM-ի վրա, ինտերֆեյսներ մեկ իրականացմամբ). ավելացրեք դրանք, երբ ի հայտ գա երկրորդ կարիքը։

Այլընտրանքներ

  • Ֆրեյմվորքի default-ներ

    Laravel-ի կամ Next.js-ի սովորական կառուցվածքը. լավ է փոքր հավելվածների համար, աճելուն զուգընթաց ավելացրեք դոմենային թղթապանակներ։

  • Hexagonal / clean architecture

    Խիստ ports and adapters. ուժեղ մեկուսացում, ավելի շատ boilerplate։

  • Միկրոսերվիսներ

    Առանձին deploy արվող միավորներ յուրաքանչյուր դոմենի համար. արժե միայն հստակ սահմանների և թիմի մեծ մասշտաբի դեպքում։

  • Polyrepo

    Մեկ ռեպո յուրաքանչյուր հավելվածի համար. ավելի պարզ թույլտվություններ, բայց ավելի դժվար է կոդ կիսելը և պայմանագրերը միասին փոխելը։

Լավագույն պրակտիկաներ

  1. Կոնտրոլլերները պահեք բարակ, իսկ բիզնես տրամաբանությունը՝ սերվիսներում կամ use case-երում։
  2. Վալիդացրեք յուրաքանչյուր մուտքի կետում և ներս փոխանցեք տիպավորված DTO-ներ։
  3. Կազմակերպեք ըստ դոմենային մոդուլների և յուրաքանչյուր մոդուլից բացեք փոքր հանրային մակերես։
  4. Առանձին route խմբեր վեբի, մոբայլի և ադմինի համար՝ նույն սերվիսները կիսելով։
  5. Վալիդացրեք միջավայրի փոփոխականները գործարկման պահին և ձախողվեք անմիջապես։
  6. Կիսեք տիպերն ու API client-ները մոնոռեպոյի փաթեթների միջոցով։
  7. Մեծ որոշումները գրանցեք կարճ ADR-ներում։

Տարածված սխալներ

  • Բիզնես կանոններ կոնտրոլլերների, view-երի կամ React կոմպոնենտների ներսում։
  • Մեկ հսկայական «utils» կամ «helpers» թղթապանակ, որից կախված է ամեն ինչ։
  • Մոբայլ API-ն կոտրել, որովհետև այն պատասխանի կառուցվածքները կիսում է կայքի հետ։
  • Առաջին իսկ օրվանից over-engineering՝ հինգ շերտ և ինտերֆեյսներ մեկ CRUD էկրանի համար։
  • process.env կամ env() կարդալ ամբողջ կոդով մեկ՝ մեկ տիպավորված կոնֆիգի փոխարեն։

Խորը վերլուծություն

07Սահմանների պահպանում գործիքներով, ոչ թե բարի մտադրություններով

Wiki-ի դիագրամը չի կանգնեցնի երեկոյան 6-ին հոգնած developer-ին, որը import է անում մեկ այլ մոդուլի ներքին repository-ն։ Ձախողվող CI ստուգումը՝ կկանգնեցնի։ TypeScript-ում dependency-cruiser-ը ստուգում է import-ի կանոնները ամբողջ նախագծում, իսկ Nx-ը workspace-երի համար ունի enforce-module-boundaries lint կանոն։ PHP-ում Deptrac-ը սահմանում է շերտեր և թույլատրված dependency-ներ, իսկ Pest-ի architecture test-երը կանոնները ձևակերպում են որպես test-եր, օրինակ՝ «controller-ները չեն կարող օգտագործել DB facade-ը»։ Լավ առաջին կանոնների հավաքածուն փոքր է․ մոդուլները կարող են import անել մեկ այլ մոդուլի միայն public entry ֆայլը, domain կոդը չի կարող import անել HTTP կամ framework class-ներ, shared կոդը չի կարող import անել մոդուլներից։ Կանոնները CI-ին ավելացրեք warning mode-ով, ուղղեք առկա խախտումները, ապա անցեք error-ի։ Յուրաքանչյուր նոր կանոն պետք է նկարագրի իրական խնդիր, որ ունեցել եք, ոչ թե տեսական մաքրության նպատակ։

js
// .dependency-cruiser.cjs
module.exports = {
  forbidden: [
    {
      name: 'no-cross-module-internals',
      comment: 'Import other modules only through their index.ts',
      severity: 'error',
      from: { path: '^src/modules/([^/]+)/' },
      to: {
        path: '^src/modules/[^/]+/',
        pathNot: ['^src/modules/$1/', '^src/modules/[^/]+/index[.]ts$'],
      },
    },
    { name: 'shared-stays-generic', severity: 'error', from: { path: '^src/shared/' }, to: { path: '^src/modules/' } },
  ],
};

08Domain modelling․ invariant-ներ, aggregate-ներ և state machine-ներ

Invariant-ը կանոն է, որը միշտ պետք է ճիշտ լինի․ պատվերի total-ը հավասար է իր տողերի գումարին, refund արված պատվերը չի կարող վճարվել։ Aggregate-ը օբյեկտների խումբ է, որը միասին պաշտպանում է այդպիսի կանոնները, օրինակ՝ պատվերն իր տողերով, և այն փոխվում է միայն իր սեփական method-ներով, երբեք՝ մեկ այլ տեղից տողի գրառումն ուղղակի խմբագրելով։ Status դաշտերը այն տեղն են, որտեղ թաքնված են bug-երի մեծ մասը, ուստի մոդելավորեք դրանք որպես բացահայտ state machine-ներ՝ թույլատրված անցումների աղյուսակ և մեկ function, որը կիրառում է դրանք։ Այդ դեպքում «կարելի՞ է refund անել չեղարկված պատվերը» հարցն ունի մեկ պատասխան մեկ տեղում, և կարող եք test անել յուրաքանչյուր զույգ։ Միավորեք սա value object-ների հետ գումարի (ամբողջ թիվ minor unit-ներով գումարած արժույթ) և քանակների համար, և bug-երի շատ դասեր, օրինակ՝ float-ի կլորացումը կամ բացասական stock-ը, անհետանում են կառուցվածքով։

ts
type OrderStatus = 'draft' | 'placed' | 'paid' | 'fulfilled' | 'cancelled' | 'refunded';
 
const allowed: Record<OrderStatus, OrderStatus[]> = {
  draft: ['placed', 'cancelled'],
  placed: ['paid', 'cancelled'],
  paid: ['fulfilled', 'refunded'],
  fulfilled: ['refunded'],
  cancelled: [],
  refunded: [],
};
 
export function transition(order: Order, to: OrderStatus): Order {
  if (!allowed[order.status].includes(to)) {
    throw new DomainError('Cannot move order from ' + order.status + ' to ' + to);
  }
  return { ...order, status: to, version: order.version + 1 };
}

09Ինչպես են մոդուլները շփվում․ ուղղակի կանչեր, event-ներ և read model-ներ

Մոդուլները պետք է համագործակցեն՝ առանց միմյանց ներսը իմանալու։ Օգտագործեք մեկ այլ մոդուլի public service-ի ուղղակի կանչ, երբ պատասխանն անմիջապես պետք է, օրինակ՝ checkout-ը pricing-ից գնանշում է խնդրում։ Օգտագործեք domain event, երբ ինչ-որ բան տեղի է ունեցել, և մյուսները կարող են արձագանքել, օրինակ՝ OrderPaid-ը գործարկում է loyalty միավորներ, անդորրագրի email և stock-ի շարժ․ order մոդուլին պետք չէ իմանալ, թե ով է լսում։ Մեկ process-ի ներսում event-ները կարող են dispatch արվել commit-ից հետո, իսկ երբ listener-ներն ապրում են մեկ այլ process-ում, գրեք դրանք outbox-ի մեջ նույն transaction-ում։ Խուսափեք նրանից, որ երկու մոդուլ գրեն նույն աղյուսակում, քանի որ այդ դեպքում ոչ մեկը չի տիրապետում կանոններին։ Հաշվետվությունները, որոնց պետք են տվյալներ շատ մոդուլներից, հատուկ դեպք են․ կառուցեք read model՝ denormalised աղյուսակ, որը թարմացվում է event-ներից կամ գիշերային job-ով, ամեն մոդուլի աղյուսակները join անող մեկ հսկա query-ի փոխարեն։

  • Command՝ «արա սա» մեկ սեփականատիրոջը, կարող է ձախողվել, պատասխան է պետք։
  • Event՝ «սա տեղի ունեցավ», զրո կամ ավելի listener, անցյալ ժամանակով անուն։
  • Read model՝ query-ների համար օպտիմալացված պատճեն, eventually consistent, վերակառուցվում է source of truth-ից։

10Legacy համակարգերի փոփոխում․ seam-եր, strangler fig և anti-corruption layer-ներ

Big-bang rewrite-երը սովորաբար ձախողվում են․ հին համակարգը շարունակում է փոխվել, իսկ նորը հետապնդում է այն։ Strangler fig pattern-ը համակարգը փոխարինում է կտոր առ կտոր։ Առջևում դրեք routing շերտ, օրինակ՝ reverse proxy կամ նոր app-ի router-ը, միաժամանակ տեղափոխեք մեկ endpoint կամ էկրան, իսկ մնացածն ուղարկեք հին կոդին, մինչև ոչինչ չմնա։ Ռիսկային կոդին ձեռք տալուց առաջ գրեք characterization test-եր, որոնք գրանցում են, թե այն ինչ է անում այսօր, նույնիսկ եթե այդ վարքը սխալ է թվում, որպեսզի refactoring-ը փոխի կառուցվածքը, ոչ թե արդյունքները։ Երբ նոր կոդը պետք է շփվի legacy model-ի կամ տարօրինակ անուններով ու կանոններով արտաքին ERP-ի հետ, մեջտեղում դրեք anti-corruption layer՝ թարգմանիչ, որը նրանց հասկացությունները քարտեզագրում է ձերի վրա, որպեսզի նրանց խառնաշփոթը չթափանցի ձեր domain։ Feature flag-երը թույլ են տալիս traffic-ը փոխել աստիճանաբար և rollback անել ակնթարթորեն։

Իրական իրավիճակներ

01

400 տողանոց controller, որին ոչ ոք չի ուզում ձեռք տալ

Խնդիր

Checkout controller-ը հասել է 400 տողի՝ խառնելով validation, pricing, stock, վճարման կանչեր և email-ներ։ Յուրաքանչյուր փոփոխություն ինչ-որ բան կոտրում է, և նոր developer-ները խուսափում են դրանից։

Ինչպես լուծել

Մի վերագրեք այն մեկ անգամից։ Նախ ամրագրեք այսօրվա վարքը feature test-երով հիմնական ճանապարհների համար՝ հաջողություն, stock-ը վերջացել է, անվավեր coupon, վճարման ձախողում։ Ապա քայլ առ քայլ առանձնացրեք․ validation-ը տեղափոխեք FormRequest կամ schema, pricing-ը՝ PricingService, ամբողջ flow-ը՝ PlaceOrder action, որը controller-ը կանչում է, իսկ email-ներն ու stock-ի թարմացումները տեղափոխեք commit-ից հետո dispatch արվող OrderPlaced event-ի listener-ների մեջ։ Յուրաքանչյուր փոքր քայլից հետո գործարկեք test-երը և յուրաքանչյուր քայլ թողարկեք առանձին։ Controller-ը վերջում մոտ քսան տող է լինում, և նույն action-ն այժմ սպասարկում է mobile և admin ալիքները։

02

Breaking change, մինչ app-ի հին տարբերակները դեռ օգտագործվում են

Խնդիր

Նոր feature-ի համար պատվերի response-ը պետք է փոխի ձևը, բայց mobile app-ի 1-ին տարբերակը դեռ տեղադրված է հազարավոր հեռախոսներում, որոնց հնարավոր չէ ստիպել թարմացվել։

Ինչպես լուծել

Նախընտրեք ավելացնող փոփոխություններ․ ավելացրեք նոր դաշտեր և պահեք հները, քանի որ հին client-ներն անտեսում են այն, ինչ չգիտեն։ Եթե ձևն իսկապես պետք է փոխվի, ստեղծեք v2 route group, որի controller-ները կանչում են նույն service-ները և օգտագործում նոր Resource, իսկ v1-ը թողեք անփոփոխ։ App-ի տարբերակն ուղարկեք header-ով և log արեք օգտագործումն ըստ տարբերակի, որպեսզի իմանաք, թե երբ է v1-ի traffic-ը բավականաչափ ցածր։ Հեռացումը հայտարարեք Deprecation և Sunset response header-ներով և app-ի ներսում թարմացման հուշումով, ավելացրեք նվազագույն աջակցվող տարբերակի ստուգում և v1-ը ջնջեք միայն համաձայնեցված ամսաթվից հետո։ Յուրաքանչյուր տարբերակի contract test-երը կանխում են պատահական կոտրումները։

03

Monorepo-ի CI-ն այժմ տևում է 25 րոպե

Խնդիր

Monorepo-ն աճել է մինչև վեց app և տասը package։ Յուրաքանչյուր pull request build և test է անում ամեն ինչ, ուստի նույնիսկ տառասխալի ուղղումը 25 րոպե սպասում է կանաչ ստուգումների։

Ինչպես լուծել

Գործարկեք միայն այն, ինչ փոխվել է։ Turborepo-ով օգտագործեք turbo run lint test build --affected, որպեսզի գործարկվեն միայն branch-ի կողմից ազդված package-ները և դրանցից կախվածները, և միացրեք remote caching, որպեսզի չփոխված task-երը վերականգնվեն՝ կրկին գործարկվելու փոխարեն։ Համոզվեք, որ յուրաքանչյուր task ճիշտ է հայտարարում իր output-ները և input-ները, այլապես caching-ը սխալ կլինի։ Pipeline-ը բաժանեք զուգահեռ job-երի lint-ի, type-երի, unit test-երի և E2E-ի համար, իսկ դանդաղ E2E suite-երը տեղափոխեք merge queue կամ գիշերային գործարկում։ Ստուգեք նաև dependency graph-ը․ shared package-ը, որը յուրաքանչյուր app import է անում մեկ helper-ի համար, ամբողջական rebuild-ներ է առաջացնում, ուստի բաժանեք այն ավելի փոքր package-ների։

Արագ ուղեցույց

Task-եր գործարկել միայն այս branch-ում փոխված package-ների համար
turbo run lint test build --affected
Build անել մեկ app և այն ամենը, ինչից այն կախված է
pnpm --filter @app/api... build
Workspace package ավելացնել որպես dependency
pnpm --filter @app/web add @app/types@workspace:*
Ստուգել TypeScript import-ի կանոնները
npx depcruise src --config .dependency-cruiser.cjs
Ստուգել PHP շերտերի կանոնները
vendor/bin/deptrac analyse
Pest architecture կանոն
arch()->expect('App\Domain')->not->toUse('Illuminate\Http');
Config-ը validate անել մեկ անգամ startup-ի ժամանակ
export const env = EnvSchema.parse(process.env);
Architecture decision record-ի անվանում
docs/adr/0007-use-outbox-for-domain-events.md

Իմ իրական նախագծերում

  • Ռեստորանային ցանցի հարթակում Laravel կոնտրոլլերները բաժանեցի Shared, Ui, Mobile և Admin շերտերի, որպեսզի վեբն ու մոբայլը վերաօգտագործեն հիմնական տրամաբանությունը՝ առանձին ալիքներով։
  • Կենդանիների ապրանքների հարթակում հաճախորդի route-երը սահմանված են մեկ անգամ և միացված են և՛ web, և՛ mobile namespace-ների տակ, սերվիսները շերտավորված են դոմենային մոդելների վրա, իսկ գնագոյացումը մոդելավորված է որպես value object-ներ։
  • Multi-tenant POS SaaS-ը, որը համատեղ ղեկավարում եմ, pnpm և Turborepo մոնոռեպո է՝ 6 հավելվածով և 10 ընդհանուր փաթեթով։
  • Սպասարկում եմ ներքին փաթեթների մոնոռեպո՝ մոտ 10 վերաօգտագործելի փաթեթով (account, admin UI, forms, core, e-commerce), որոնք օգտագործվում են մոտ 10 հաճախորդային կայքում։

Հարցեր և պատասխաններ

Q1Ինչպե՞ս եք կառուցում բեքենդ նախագիծը զրոյից։

Սկսում եմ որպես մոդուլային մոնոլիթ՝ կազմակերպված ըստ դոմենի՝ orders, catalog, customers, payments, յուրաքանչյուրն իր կոնտրոլլերներով, սերվիսներով, DTO-ներով և տվյալների հասանելիությամբ՝ միասին։ Կոնտրոլլերները բարակ են՝ վալիդացնել մուտքը, կանչել սերվիսը, վերադարձնել resource։ Սերվիսները պահում են բիզնես կանոնները և ոչինչ չգիտեն HTTP-ի մասին։ Cross-cutting հարցերը, ինչպիսիք են auth-ը, լոգավորումը, կոնֆիգը և գումարի հետ աշխատանքը, գնում են shared շերտ։ Կոնֆիգուրացիան գալիս է վալիդացված միջավայրի փոփոխականներից։ Առաջին օրվանից ավելացնում եմ թեստեր սերվիսների մակարդակում և feature թեստեր հիմնական endpoint-ների համար։ Եթե հետագայում որևէ դոմենի պետք լինի անկախ մասշտաբավորում, մաքուր սահմանները դրա առանձնացումը իրատեսական են դարձնում։

Q2Ի՞նչ է DTO-ն և ինչու՞ օգտագործել այն։

Data Transfer Object-ը պարզ տիպավորված օբյեկտ է, որը ճշգրիտ նկարագրում է, թե ինչ տվյալներ է գործողությունն ընդունում կամ վերադարձնում, առանց որևէ վարքագծի։ Հարցման «հում» զանգվածը սերվիսին փոխանցելու փոխարեն դուք վալիդացնում եք հարցումը և այն քարտեզագրում, ասենք, CreateOrderDto-ի։ Առավելությունները՝ սերվիսի պայմանագիրը բացահայտ է և ստուգվում է տիպերով, անհայտ դաշտերը չեն կարող գաղտագողի ներս մտնել, նույն սերվիսը կարելի է կանչել HTTP-ից, CLI-ից կամ հերթից նույն օբյեկտով, և թեստերը հեշտ է գրել։ NestJS-ում DTO-ները class-validator դեկորատորներով կլասներ են, Laravel-ում այդ դերը կատարում է FormRequest-ը կամ data class-ը։

Q3Արդյո՞ք վեբը, մոբայլը և ադմինը պետք է օգտագործեն նույն API-ն։

Նրանք պետք է կիսեն բիզնես տրամաբանությունը, բայց սովորաբար ոչ նույն endpoint-ները։ Մոբայլ հավելվածները տեղադրված են մնում ամիսներով, ուստի նրանց API-ին պետք է բացահայտ տարբերակավորում և հետադարձ համատեղելիություն։ Ադմինի endpoint-ներին պետք են ավելի խիստ թույլտվություններ, audit լոգեր և այլ կառուցվածքներ, օրինակ՝ զանգվածային գործողություններ և ֆիլտրեր։ Կայքը կարող է օգտագործել cookie սեսիաներ, մինչդեռ հավելվածը՝ թոքեններ։ Ուստի յուրաքանչյուր ալիքի համար ստեղծում եմ առանձին route խմբեր կամ կոնտրոլլերներ, որոնք կանչում են նույն սերվիսները։ Որտեղ ֆունկցիան իսկապես նույնական է վեբի և մոբայլի հաճախորդների համար, սահմանում եմ այն մեկ անգամ և միացնում երկու prefix-ների տակ։ Այդպես պահպանվում է ճշմարտության մեկ աղբյուր՝ առանց թողարկման ցիկլերը իրար կապելու։

Q4Ե՞րբ է մոնոռեպոն լավ գաղափար։

Երբ մի քանի հավելված կիսում են կոդ և փոխվում են միասին՝ API, վեբ հավելված, ադմին վահանակ և մոբայլ հավելված, որոնք կիսում են տիպեր, վալիդացիայի սխեմաներ, API client և UI կոմպոնենտներ։ Մոնոռեպոն թույլ է տալիս փոխել API պայմանագիրը և նրա սպառողները մեկ pull request-ում, գործարկել միայն ազդեցության տակ ընկած թեստերը և պահել գործիքակազմը միատեսակ։ pnpm workspaces-ը Turborepo-ի կամ Nx-ի հետ միասին լուծում են կապակցումը և առաջադրանքների քեշավորված գործարկումը։ Այն պակաս օգտակար է, երբ հավելվածները պատկանում են տարբեր թիմերի՝ առանձին թողարկման գործընթացներով, կամ երբ հասանելիությունը պետք է սահմանափակվի ըստ նախագծի։ Այդ դեպքում ավելի լավ են աշխատում առանձին ռեպոները՝ հրապարակված փաթեթներով։

Q5Ինչպե՞ս եք կառավարում կոնֆիգուրացիան տարբեր միջավայրերում։

Նույն կոդն ամենուր, կոնֆիգուրացիան՝ միջավայրի փոփոխականներից, twelve-factor սկզբունքներին համապատասխան։ Փոփոխականները կարդում եմ մեկ տեղում՝ գործարկման պահին, և վալիդացնում եմ սխեմայով, օրինակ՝ zod-ով կամ Laravel-ի config ֆայլերով, անմիջապես ձախողվելով, եթե որևէ բան բացակայում է կամ սխալ ձևաչափ ունի։ Մնացած կոդը կարդում է տիպավորված կոնֆիգը, երբեք ուղղակիորեն միջավայրը։ Գաղտնիքները գալիս են հոսթինգ պլատֆորմից կամ secrets manager-ից, երբեք ռեպոզիտորիայից. ռեպոյում կա .env.example՝ placeholder-ներով։ Feature flag-երը ծածկում են այն վարքագիծը, որը պետք է փոխվի առանց deploy-ի։ Յուրաքանչյուր միջավայր՝ local, staging և production, ունի իր սեփական credentials-ը և տվյալների բազաները։

Սենիոր մակարդակի հարցեր

S1Ե՞րբ կբաժանեիք modular monolith-ը առանձին service-ների։

Միայն երբ կոնկրետ ուժն արդարացնում է արժեքը։ Լավ պատճառներ․ մոդուլին պետք է շատ տարբեր մասշտաբավորում, օրինակ՝ image pipeline կամ realtime gateway, առանձին թիմին պետք է անկախ deploy անել, մասին պետք է մեկուսացում անվտանգության կամ compliance-ի համար, օրինակ՝ քարտի տվյալներ, կամ մեկ այլ runtime ավելի լավ է համապատասխանում։ Վատ պատճառներ․ դա ժամանակակից է թվում, կամ կոդը խառնաշփոթ է, քանի որ խառնաշփոթ մոդուլը դառնում է խառնաշփոթ service՝ ավելացված ցանցային կանչերով։ Բաժանելուց առաջ մոդուլն արդեն պետք է շփվի միայն public interface-ի և event-ների միջոցով, տիրապետի իր աղյուսակներին և չունենա միջմոդուլային transaction-ներ։ Այդ դեպքում առանձնացումը հիմնականում կոդի տեղափոխում է և process-ի ներսի կանչերի փոխարինում HTTP-ով կամ message-ներով, գումարած retry-եր, timeout-ներ, idempotency և tracing, ինչը իրական շարունակական արժեքն է։

S2Արժե՞ արդյոք clean կամ hexagonal architecture-ը տիպիկ պրոդուկտի համար։

Հիմնական գաղափարը միշտ արժե․ բիզնես կանոնները չպետք է կախված լինեն HTTP-ից, ORM-ից կամ vendor SDK-ից, և ինտեգրացիաները պետք է նստած լինեն ձեզ պատկանող interface-ների հետևում։ Լրիվ արարողակարգը հաճախ չարժե։ Eloquent-ը կամ Prisma-ն մեկ implementation ունեցող repository-ների մեջ փաթաթելը, յուրաքանչյուր entity-ն երկու անգամ map անելը և յուրաքանչյուր class-ի համար port ավելացնելը եռապատկում է CRUD էկրանի կոդը և դանդաղեցնում թիմը։ Ես կիրառում եմ այն ընտրողաբար՝ խիստ port-եր և adapter-ներ վճարումների, ERP-ների, SMS-ի և այն ամենի շուրջ, ինչը կարող եմ փոխարինել կամ պետք է fake անեմ test-երում, և մաքուր domain շերտ բարդ կանոնների համար, ինչպես pricing-ը կամ loyalty-ն։ Պարզ admin CRUD-ը մնում է framework-ին մոտ։ Ստուգումն այն է, թե արդյոք abstraction-ն ունի երկրորդ իրական կիրառություն, օրինակ՝ test fake կամ պլանավորված երկրորդ provider։

S3Ինչպե՞ս եք տվյալները համաձայնեցված պահում մոդուլների միջև առանց ընդհանուր transaction-ների։

Մեկ database ունեցող monolith-ի ներսում դեռ կարող եք օգտագործել մեկ transaction, բայց ես խուսափում եմ նրանից, որ մեկ transaction գրի մի քանի մոդուլների աղյուսակներում, քանի որ դա կապում է դրանց schema-ները։ Փոխարենը տեր մոդուլը commit է անում իր փոփոխությունը և նույն transaction-ում domain event է գրանցում outbox աղյուսակում։ Մյուս մոդուլներն արձագանքում են outbox-ից asynchronous և idempotent կերպով։ Դա համակարգը դարձնում է eventually consistent, ուստի UI-ն և process-ները պետք է ընդունեն կարճ ուշացում, օրինակ՝ loyalty միավորները հայտնվում են մեկ վայրկյան անց։ Բազմաքայլ բիզնես process-ների համար, ինչպես stock-ի ամրագրումը, գանձումը, ապա հաստատումը, մոդելավորում եմ saga՝ բացահայտ compensation քայլերով, այլ ոչ թե ձևացնում, թե այն atomic է։ Trade-off-ը ավելի շատ նախնական դիզայնն է՝ փոխարենը ստանալով մոդուլներ, որոնք կարող են զարգանալ և հետագայում մաքուր բաժանվել։

S4Ինչպե՞ս եք կառավարում հաշվետվությունները շատ մոդուլների միջով։

Հաշվետվություններն այն տեղն են, որտեղ սահմանները սովորաբար կոտրվում են, քանի որ ինչ-որ մեկը գրում է մեկ SQL query, որը join է անում ամեն մոդուլի աղյուսակները, և այժմ ոչ մի մոդուլ չի կարող փոխել իր schema-ն։ Հաշվետվությունների կարդալները առանձնացնում եմ transactional model-ից։ Փոքր պրոդուկտները կարող են օգտագործել database view-ներ, որոնք պատկանում են reporting մոդուլին, ինչը schema-ների փոփոխության դեպքում թարմացնելու մեկ տեղ է տալիս։ Ավելի մեծերը ստանում են read model-ներ՝ denormalised աղյուսակներ կամ առանձին analytics database, որոնք սնվում են domain event-ներից կամ պլանավորված extract-ներից և ձևավորված են ճիշտ dashboard-ների համար։ Դրանք eventually consistent են և կարող են վերակառուցվել source of truth-ից, ինչը նաև ծանր հաշվետվությունները անվնաս է դարձնում production database-ի համար։ Trade-off-ը pipeline-ի սպասարկումն է և թեթև հնացածությունը, որը բիզնես հաշվետվությունների մեծ մասը հեշտությամբ ընդունում է։

Բառարան

Bounded context
Համակարգի մաս, որտեղ բառն ունի մեկ ճշգրիտ իմաստ, օրինակ՝ «customer»-ը billing-ում և support-ում։
Invariant
Բիզնես կանոն, որը միշտ պետք է գործի, օրինակ՝ «stock-ը երբեք զրոյից ցածր չի իջնում»։
Aggregate
Օբյեկտների խումբ, որը փոխվում է միասին մեկ մուտքի կետով, որպեսզի իր invariant-ները ճիշտ մնան։
Value object
Փոքր immutable տիպ, որը սահմանվում է իր արժեքով, օրինակ՝ Money կամ EmailAddress, և ինքն իրեն validate է անում։
Read model
Տվյալների պատճեն՝ ձևավորված արագ query-ների կամ հաշվետվությունների համար, թարմացվող source of truth-ից։
Anti-corruption layer
Թարգմանության շերտ, որը թույլ չի տալիս արտաքին կամ legacy model-ին թափանցել ձեր սեփական domain։
Strangler fig
Հին համակարգի աստիճանական փոխարինում՝ feature-ները մեկ առ մեկ նոր կոդին ուղղորդելով։
Characterization test
Test, որը գրանցում է, թե առկա կոդն այսօր ինչ է անում, որպեսզի refactoring-ը կարողանա ապացուցել, որ ոչինչ չի փոխվել։
Cohesion և coupling
Cohesion՝ որքան են կապված մեկ մոդուլի ներսի բաները։ Coupling՝ որքան են մոդուլները կախված միմյանցից։ Ձգտեք բարձրի և ցածրի։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։