├── .prettierrc.yaml ├── .husky ├── pre-commit └── commit-msg ├── ModularMonolith ├── Monolith.ArchitectureTests │ ├── GlobalUsings.cs │ ├── ModulesTests.cs │ ├── Modules │ │ ├── AssemblyReferencesAccessor.cs │ │ └── Module.cs │ └── Monolith.ArchitectureTests.csproj ├── Audit │ ├── Audit.Domain │ │ ├── Class1.cs │ │ └── Audit.Domain.csproj │ └── Audit.Contracts │ │ ├── Class1.cs │ │ └── Audit.Contracts.csproj ├── Common │ ├── Common.Domain │ │ ├── Class1.cs │ │ └── Common.Domain.csproj │ └── Common.Contracts │ │ ├── Class1.cs │ │ └── Common.Contracts.csproj ├── Emails │ ├── Emails.Domain │ │ ├── Class1.cs │ │ └── Emails.Domain.csproj │ └── Emails.Contracts │ │ ├── Class1.cs │ │ └── Emails.Contracts.csproj ├── Schedule │ ├── Shedule.Domain │ │ ├── Class1.cs │ │ └── Shedule.Domain.csproj │ ├── Shedule.Contract │ │ ├── Class1.cs │ │ └── Shedule.Contract.csproj │ └── Shedule.DataAccess │ │ ├── Class1.cs │ │ └── Shedule.DataAccess.csproj ├── Monolith.Api │ ├── Program.cs │ └── Monolith.Api.csproj ├── Customers │ ├── Byndyusoft.Customers.Domain │ │ ├── Class1.cs │ │ └── Byndyusoft.Customers.Domain.csproj │ ├── Byndyusoft.Customers.Contracts │ │ ├── Class1.cs │ │ └── Byndyusoft.Customers.Contracts.csproj │ ├── Byndyusoft.Customers.DataAccess │ │ ├── Class1.cs │ │ └── Byndyusoft.Customers.DataAccess.csproj │ └── Byndyusoft.Customers.Processing │ │ ├── Class1.cs │ │ └── Byndyusoft.Customers.Processing.csproj ├── ProjectQuality │ ├── Byndyusoft.ProjectQuality.Domain │ │ ├── Class1.cs │ │ └── Byndyusoft.ProjectQuality.Domain.csproj │ ├── Byndyusoft.ProjectQuality.Contracts │ │ ├── Class1.cs │ │ └── Byndyusoft.ProjectQuality.Contracts.csproj │ └── Byndyusoft.ProjectQuality.DataAccess │ │ ├── Class1.cs │ │ └── Byndyusoft.ProjectQuality.DataAccess.csproj ├── Readme.md ├── ModularMonolith.sln ├── Architecture.svg └── DetailedArchitecture.svg ├── src ├── plantuml │ ├── index.ts │ ├── loadPlantumlElements.ts │ ├── lib │ │ ├── groupElements.ts │ │ └── filterElements.ts │ └── mapContainersFromPlantumlElements.ts ├── entities │ ├── section.ts │ ├── index.ts │ ├── relation.ts │ ├── pumlFile.ts │ ├── boundary.ts │ └── container.ts ├── deployConfigs │ ├── index.ts │ ├── deployConfig.ts │ ├── loadMicroserviceDeployConfigs.ts │ └── mapContainersFromDeployConfigs.ts └── analyzer.ts ├── resources ├── kubernetes │ └── microservices │ │ ├── stock-acl.yml │ │ ├── task-repository.yml │ │ ├── invoice-repository.yml │ │ ├── goods-acl.yml │ │ ├── invoice-acl.yml │ │ ├── bff.yml │ │ └── camunda.yml └── architecture │ ├── boundaries.puml │ ├── banking │ ├── C4L1.puml │ ├── C4L3.puml │ └── C4L2.puml │ ├── generated.puml │ ├── C4L2.puml │ ├── Demo Coupling And Cohesion.svg │ ├── Demo Generated.svg │ └── Demo Tests.svg ├── tsconfig.json ├── ADRs ├── ADR template.md ├── Anti-corruption Layer.md └── Database per CRUD-service.md ├── .eslintrc.json ├── .github └── workflows │ ├── test.yaml │ └── main.yml ├── test ├── acl.test.ts ├── acyclic.test.ts ├── crud.test.ts ├── ccr.test.ts ├── boundaries.test.ts └── architecture.test.ts ├── roadmap.md ├── patterns.md ├── package.json ├── .gitignore └── README.md /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: all 2 | endOfLine: auto 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /ModularMonolith/Monolith.ArchitectureTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using FluentAssertions; 3 | -------------------------------------------------------------------------------- /src/plantuml/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./loadPlantumlElements"; 2 | export * from "./mapContainersFromPlantumlElements"; 3 | -------------------------------------------------------------------------------- /src/entities/section.ts: -------------------------------------------------------------------------------- 1 | export interface Section { 2 | readonly name: string; 3 | readonly prod_value: string; 4 | } 5 | -------------------------------------------------------------------------------- /ModularMonolith/Audit/Audit.Domain/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Audit.Domain 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Audit/Audit.Contracts/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Audit.Contracts 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Common/Common.Domain/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Domain 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Emails/Emails.Domain/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Emails.Domain 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Schedule/Shedule.Domain/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Shedule.Domain 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Common/Common.Contracts/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Contracts 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Emails/Emails.Contracts/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Emails.Contracts 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Monolith.Api/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | Console.WriteLine("Hello, World!"); 3 | -------------------------------------------------------------------------------- /ModularMonolith/Schedule/Shedule.Contract/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Shedule.Contract 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Schedule/Shedule.DataAccess/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Shedule.DataAccess 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./container"; 2 | export * from "./section"; 3 | export * from "./boundary"; 4 | export * from "./pumlFile"; 5 | -------------------------------------------------------------------------------- /ModularMonolith/Monolith.ArchitectureTests/ModulesTests.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Byndyusoft/aact/HEAD/ModularMonolith/Monolith.ArchitectureTests/ModulesTests.cs -------------------------------------------------------------------------------- /resources/kubernetes/microservices/stock-acl.yml: -------------------------------------------------------------------------------- 1 | name: stock-acl 2 | 3 | environment: 4 | STOCK_BASE_URL: 5 | default: https://gateway.int.com:443/stock/v1 6 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.Domain/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.Customers.Domain 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/deployConfigs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deployConfig"; 2 | export * from "./loadMicroserviceDeployConfigs"; 3 | export * from "./mapContainersFromDeployConfigs"; 4 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.Contracts/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.Customers.Contracts 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.DataAccess/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.Customers.DataAccess 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.Processing/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.Customers.Processing 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/ProjectQuality/Byndyusoft.ProjectQuality.Domain/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.ProjectQuality.Domain 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/ProjectQuality/Byndyusoft.ProjectQuality.Contracts/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.ProjectQuality.Contracts 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModularMonolith/ProjectQuality/Byndyusoft.ProjectQuality.DataAccess/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Byndyusoft.ProjectQuality.DataAccess 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /resources/kubernetes/microservices/task-repository.yml: -------------------------------------------------------------------------------- 1 | name: task-repository 2 | 3 | environment: 4 | PG_CONNECTION_STRING: 5 | default: "postgresql://task-repo:pass-task-repo@postgresql:543/task-repo" -------------------------------------------------------------------------------- /src/entities/relation.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./container"; 2 | 3 | export interface Relation { 4 | readonly to: Container; 5 | readonly technology?: string; 6 | readonly tags?: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /resources/kubernetes/microservices/invoice-repository.yml: -------------------------------------------------------------------------------- 1 | name: invoice-repository 2 | 3 | environment: 4 | PG_CONNECTION_STRING: 5 | default: "postgresql://invoice-repo:pass-invoice-repo@postgresql:543/invoice-repo" -------------------------------------------------------------------------------- /src/entities/pumlFile.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./container"; 2 | import { Boundary } from "./boundary"; 3 | 4 | export interface PumlFile { 5 | readonly boundaries: Boundary[]; 6 | readonly allContainers: Container[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/deployConfigs/deployConfig.ts: -------------------------------------------------------------------------------- 1 | import { Section } from "../entities"; 2 | 3 | export interface DeployConfig { 4 | name: string; 5 | fileName?: string; 6 | readonly environment?: { [key: string]: object }; 7 | sections: Section[]; 8 | } 9 | -------------------------------------------------------------------------------- /resources/kubernetes/microservices/goods-acl.yml: -------------------------------------------------------------------------------- 1 | name: goods-acl 2 | 3 | environment: 4 | GOODS_REPOSITORY_BASE_URL: 5 | default: https://gateway.int.com:443/goods/v1 6 | GOODS2_REPOSITORY_BASE_URL: 7 | default: https://gateway.int.com:443/goods2/v1 -------------------------------------------------------------------------------- /src/entities/boundary.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./container"; 2 | 3 | 4 | export interface Boundary { 5 | readonly name: string; 6 | readonly label: string; 7 | readonly type?: string; 8 | boundaries: Boundary[]; 9 | readonly containers: Container[]; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /ModularMonolith/Audit/Audit.Domain/Audit.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Audit/Audit.Contracts/Audit.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Common/Common.Domain/Common.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Emails/Emails.Domain/Emails.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Schedule/Shedule.Domain/Shedule.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/entities/container.ts: -------------------------------------------------------------------------------- 1 | import { Relation } from "./relation"; 2 | 3 | export interface Container { 4 | readonly name: string; 5 | readonly label: string; 6 | readonly type?: string; 7 | readonly tags?: string; 8 | readonly description: string; 9 | readonly relations: Relation[]; 10 | } -------------------------------------------------------------------------------- /ModularMonolith/Common/Common.Contracts/Common.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Emails/Emails.Contracts/Emails.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Schedule/Shedule.Contract/Shedule.Contract.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@byndyusoft/tsconfig/tsconfig.node16.json", 3 | "compilerOptions": { 4 | // Language and Environment 5 | "lib": ["es2021", "dom"], 6 | "esModuleInterop": true 7 | }, 8 | "exclude": ["./dist", "./node_modules"], 9 | "include": ["./**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ModularMonolith/Schedule/Shedule.DataAccess/Shedule.DataAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.Domain/Byndyusoft.Customers.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.Contracts/Byndyusoft.Customers.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.DataAccess/Byndyusoft.Customers.DataAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/Customers/Byndyusoft.Customers.Processing/Byndyusoft.Customers.Processing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/ProjectQuality/Byndyusoft.ProjectQuality.Domain/Byndyusoft.ProjectQuality.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/ProjectQuality/Byndyusoft.ProjectQuality.Contracts/Byndyusoft.ProjectQuality.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ModularMonolith/ProjectQuality/Byndyusoft.ProjectQuality.DataAccess/Byndyusoft.ProjectQuality.DataAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/kubernetes/microservices/invoice-acl.yml: -------------------------------------------------------------------------------- 1 | name: invoice-acl 2 | 3 | environment: 4 | EXT_SYSTEM_BASE_URL: 5 | default: https://gateway.int.com:443/ext/v1 6 | KAFKA_INVOICE_BROKERS: 7 | default: msg-q8s.int.cloud:9092 8 | KAFKA_INVOICE_INPUT_TOPIC: 9 | default: orig-invoice-q8s-v1 10 | KAFKA_INVOICE_OUTPUT_TOPIC: 11 | default: result-invoice-q8s-v1 12 | -------------------------------------------------------------------------------- /resources/kubernetes/microservices/bff.yml: -------------------------------------------------------------------------------- 1 | name: bff 2 | 3 | environment: 4 | INVOICE_REPOSITORY_API_CLIENT_BASE_URL: 5 | default: http://invoice-repository:8080 6 | TASK_REPOSITORY_API_CLIENT_BASE_URL: 7 | default: http://task-repository:8080 8 | CAMUNDA_API_CLIENT_BASE_URL: 9 | default: http://camunda:8080 10 | GOODS_ACL_API_CLIENT_BASE_URL: 11 | default: http://goods-acl:8080 -------------------------------------------------------------------------------- /ADRs/ADR template.md: -------------------------------------------------------------------------------- 1 | # <Название> 2 | ## Контекст 3 | <описание контекста, в рамках которого применяется решение> 4 | ## Краткое описание решения и его обоснование 5 | ### Схема 6 | <архитектурная схема решения> 7 | ### Решение 8 | <описание решения> 9 | ### Обоснование 10 | <обоснование решения> 11 | ## Как покрыть тестами 12 | <описание механизма тестирования> 13 | ### Примеры тестов 14 | <ссылки на тесты> 15 | -------------------------------------------------------------------------------- /resources/kubernetes/microservices/camunda.yml: -------------------------------------------------------------------------------- 1 | name: camunda 2 | 3 | environment: 4 | INVOICE_REPOSITORY_API_CLIENT_BASE_URL: 5 | default: http://invoice-repository:8080 6 | TASK_REPOSITORY_API_CLIENT_BASE_URL: 7 | default: http://task-repository:8080 8 | STOCK_ACL_API_CLIENT_BASE_URL: 9 | default: http://stock-acl:8080 10 | INVOICE_ACL_API_CLIENT_BASE_URL: 11 | default: http://invoice-acl:8080 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@byndyusoft/eslint-config/backend"], 4 | "parserOptions": { 5 | "ecmaVersion": 2021 // Node.js v16 LTS 6 | }, 7 | "overrides": [ 8 | { 9 | "files": ["*.ts"], 10 | "rules": { 11 | // @typescript-eslint/eslint-plugin Supported Rules 12 | "@typescript-eslint/no-non-null-assertion": "off" // type hacks 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install -g yarn 20 | - name: yarn install, build and test 21 | run: | 22 | yarn 23 | yarn test 24 | -------------------------------------------------------------------------------- /test/acl.test.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "../src/entities"; 2 | import { 3 | loadPlantumlElements, 4 | mapContainersFromPlantumlElements, 5 | } from "../src/plantuml"; 6 | 7 | const SystemExternalType = "System_Ext"; 8 | 9 | describe("Architecture", () => { 10 | let containersFromPuml: Container[]; 11 | 12 | beforeAll(async () => { 13 | const pumlElements = await loadPlantumlElements("C4L2.puml"); 14 | containersFromPuml = 15 | mapContainersFromPlantumlElements(pumlElements).allContainers; 16 | }); 17 | 18 | it("only acl can depend on external systems", () => { 19 | for (const container of containersFromPuml) { 20 | const externalRelations = container.relations.filter( 21 | (r) => r.to.type === SystemExternalType, 22 | ); 23 | if (!container.tags?.includes("acl") && externalRelations.length > 0) 24 | throw new Error('Assertion failed'); 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /resources/architecture/boundaries.puml: -------------------------------------------------------------------------------- 1 | @startuml "Demo Coupling And Cohesion" 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 3 | LAYOUT_WITH_LEGEND() 4 | 5 | System_Ext(ext, "Внешняя система") 6 | 7 | Boundary(boundary0, "Наш проект"){ 8 | 9 | Boundary(boundary1, "Контекст 1"){ 10 | Container(service1, "Микросервис A") 11 | Container(service2, "Микросервис B") 12 | Container(service3, "Микросервис C") 13 | } 14 | 15 | Boundary(boundary2, "Контекст 2"){ 16 | Container(service4, "Микросервис X") 17 | Container(service5, "Микросервис Y") 18 | } 19 | } 20 | 21 | Rel(service1, service2, "") 22 | Rel(service1, service3, "") 23 | Rel(service2, service3, "") 24 | 25 | Rel(service1, service4, "") 26 | Rel(service2, service5, "") 27 | 28 | Rel(service4, service5, "") 29 | 30 | Rel(service3, ext, "") 31 | 32 | @enduml 33 | -------------------------------------------------------------------------------- /test/acyclic.test.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "../src/entities"; 2 | import { Relation } from "../src/entities/relation"; 3 | import { 4 | loadPlantumlElements, 5 | mapContainersFromPlantumlElements, 6 | } from "../src/plantuml"; 7 | 8 | describe("Architecture", () => { 9 | let containersFromPuml: Container[]; 10 | 11 | beforeAll(async () => { 12 | const pumlElements = await loadPlantumlElements("C4L2.puml"); 13 | containersFromPuml = 14 | mapContainersFromPlantumlElements(pumlElements).allContainers; 15 | }); 16 | 17 | it("no cycles in rels", () => { 18 | for (const container of containersFromPuml) { 19 | findCycle(container.relations, container.name); 20 | } 21 | 22 | function findCycle(rels: Relation[], sourceContainerName: string) { 23 | for (const rel of rels) { 24 | if (rel.to.name == sourceContainerName) 25 | throw new Error('Assertion failed'); 26 | findCycle(rel.to.relations, sourceContainerName); 27 | } 28 | } 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /ModularMonolith/Monolith.ArchitectureTests/Modules/AssemblyReferencesAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Monolith.ArchitectureTests.Modules 2 | { 3 | using System.Reflection; 4 | 5 | /// 6 | /// Предоставляется доступ к ссылкам сборок 7 | /// 8 | public class AssemblyReferencesAccessor 9 | { 10 | private readonly Dictionary _referencesByName = []; 11 | 12 | /// 13 | /// Получить все сборки связанных с указанной сборкой 14 | /// 15 | public string[] GetReferences(string assemblyName) 16 | { 17 | if (_referencesByName.TryGetValue(assemblyName, out var references)) 18 | return references; 19 | 20 | var assembly = Assembly.Load(assemblyName); 21 | references = assembly.GetReferencedAssemblies().Select(x => x.Name!).ToArray(); 22 | _referencesByName[assembly.GetName().Name!] = references; 23 | 24 | return references; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/architecture/banking/C4L1.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml 3 | ' uncomment the following line and comment the first to use locally 4 | ' !include C4_Context.puml 5 | 6 | LAYOUT_WITH_LEGEND() 7 | 8 | title System Context diagram for Internet Banking System 9 | 10 | Person(customer, "Personal Banking Customer", "A customer of the bank, with personal bank accounts.") 11 | System(banking_system, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") 12 | 13 | System_Ext(mail_system, "E-mail system", "The internal Microsoft Exchange e-mail system.") 14 | System_Ext(mainframe, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") 15 | 16 | Rel(customer, banking_system, "Uses") 17 | Rel_Back(customer, mail_system, "Sends e-mails to") 18 | Rel_Neighbor(banking_system, mail_system, "Sends e-mails", "SMTP") 19 | Rel(banking_system, mainframe, "Uses") 20 | @enduml 21 | -------------------------------------------------------------------------------- /src/plantuml/loadPlantumlElements.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | import { Comment, parse as parsePuml, Stdlib_C4_Dynamic_Rel, UMLElement } from "plantuml-parser"; 5 | 6 | import { filterElements } from "./lib/filterElements"; 7 | 8 | const getFilepath = (fileName: string): string => { 9 | return path.join(process.cwd(), "resources/architecture", fileName); 10 | }; 11 | 12 | export const loadPlantumlElements = async ( 13 | fileName: string, 14 | ): Promise => { 15 | const filepath = getFilepath(fileName); 16 | 17 | let data = await fs.readFile(filepath, "utf8"); 18 | data = data.replaceAll(/, \$tags=(".+?")/g, ", $1").replaceAll('""', '" "'); 19 | const [{ elements }] = parsePuml(data); 20 | 21 | for (const element of elements) { 22 | if (element instanceof Comment) continue; 23 | const relation = element as Stdlib_C4_Dynamic_Rel; 24 | if(relation.type_.name.startsWith("Rel_Back")) 25 | { 26 | const from = relation.from; 27 | relation.from = relation.to; 28 | relation.to = from; 29 | } 30 | } 31 | 32 | return filterElements(elements); 33 | }; 34 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap развития инструментов (или чем можно заняться контрибьюторам) 2 | 3 | ### Покрытие архитектуры тестами 4 | ✅ Покрытие тестами микросервисной архитектуры
5 | ✅ Покрытие тестами архитектуры модульного монолита
6 | 🟩 Добавление реализаций и примеров под разные стэки (сейчас TypeScript и C#)
7 | ⌛ [Справочник](https://github.com/Byndyusoft/aact/blob/main/patterns.md) принципов и паттернов проектирования (например, в формате ADR)
8 | ⌛ Примеры тестов на пункты [справочника](https://github.com/Byndyusoft/aact/blob/main/patterns.md) 9 | 10 | ### Автогенерация 11 | ✅ Автогенерация архитектурной схемы по конфигам инфраструктуры
12 | 🟩 Автогенерация конфигов инфраструктуры по архитектурной схеме
13 | 🟩 Добавление провайдеров для различных реализаций IaC
14 | 🟩 Автогенерация и архитектурной схемы, и конфигов инфраструктуры по архитектурному решению (ADR) 15 | 16 | ### Инструменты рефакторинга микросервисной архитектуры 17 | 🟩 Изменение сигнатуры метода API (endpoint'а)
18 | 🟩 Вынос метода API (endpoint'а) микросервиса в отдельный микросервис
19 | 🟩 Inline микросервиса — поглощение микросервиса своим потребителем 20 | -------------------------------------------------------------------------------- /test/crud.test.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "../src/entities"; 2 | import { 3 | loadPlantumlElements, 4 | mapContainersFromPlantumlElements, 5 | } from "../src/plantuml"; 6 | 7 | const ContainerDb = "ContainerDb"; 8 | 9 | describe("Architecture", () => { 10 | let containersFromPuml: Container[]; 11 | 12 | beforeAll(async () => { 13 | const pumlElements = await loadPlantumlElements("C4L2.puml"); 14 | containersFromPuml = 15 | mapContainersFromPlantumlElements(pumlElements).allContainers; 16 | }); 17 | 18 | it("only crud can relate to DB", () => { 19 | for (const container of containersFromPuml) { 20 | const dbRelation = container.relations.filter( 21 | (r) => r.to.type === ContainerDb, 22 | ); 23 | if ( 24 | !container.tags?.includes("repo") && 25 | !container.tags?.includes("relay") && 26 | dbRelation.length > 0 27 | ) 28 | throw new Error('Assertion failed'); 29 | 30 | if ( 31 | container.tags?.includes("repo") && 32 | container.relations.some((r) => r.to.type != ContainerDb) 33 | ) 34 | throw new Error('Assertion failed'); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: plantuml workflow 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | plantuml: 10 | name: plantuml 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: install java 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: "17" 20 | distribution: temurin 21 | 22 | - name: install graphviz 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install graphviz 26 | 27 | - name: download plantuml 28 | run: wget -O /opt/plantuml.jar https://sourceforge.net/projects/plantuml/files/plantuml.jar/download 29 | 30 | - name: generate svg diagrams 31 | run: java -jar /opt/plantuml.jar -tsvg "./resources/architecture/*.puml" 32 | 33 | - name: push svg diagrams 34 | uses: stefanzweifel/git-auto-commit-action@v4 35 | with: 36 | commit_message: "docs: render PlantUML files" 37 | file_pattern: ./resources/architecture/*.svg 38 | commit_user_name: github-actions[bot] 39 | commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com -------------------------------------------------------------------------------- /ModularMonolith/Monolith.ArchitectureTests/Monolith.ArchitectureTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ADRs/Anti-corruption Layer.md: -------------------------------------------------------------------------------- 1 | # Anti-corruption Layer 2 | 3 | ## Контекст 4 | 5 | Микросервисная архитектура с выделенным периметром продукта, имеющим ряд интеграций с внешними системами (другими системами предприятия и/или внешними решениями) 6 | 7 | ## Краткое описание решения и его обоснование 8 | ### Схема 9 | ![image](https://github.com/Byndyusoft/aact/assets/1096954/970dcf43-d347-4294-9be0-03f042b85f8b) 10 | 11 | ### Решение 12 | 13 | Выделение отдельного слоя периметра и взаимодействия с "внешним миром", реализованного в виде микросервисов-адаптеров и BFF-микросервиса. 14 | 15 | ### Обоснование 16 | 17 | ~~Костыли~~ Нюансы внешних систем и интеграций инкапсулируются в адаптерах, не проникают внутрь периметра. Микросервисы-адаптеры отвечают за двух-стороннюю интеграцию с внешними системами. 18 | 19 | ## Как покрыть тестами 20 | 21 | 1. Помечаем на архитектуре соответствующие микросервисы признаком "Adapter" 22 | ``` 23 | Container(goods_adapter, “Goods ACL", "NestJS", $tags="adapter") 24 | ``` 25 | 2. Проверяем, что связи с внешними системами имеют только сервисы с таким признаком 26 | 27 | ### Примеры тестов 28 | 29 | [only acl can depence from external systems](https://github.com/Byndyusoft/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L111C7-L111C49) 30 | -------------------------------------------------------------------------------- /src/plantuml/lib/groupElements.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Relationship, 3 | Stdlib_C4_Container_Component, 4 | Stdlib_C4_Dynamic_Rel, 5 | UMLElement, 6 | } from "plantuml-parser"; 7 | import { Stdlib_C4_Boundary } from "plantuml-parser/dist/types"; 8 | 9 | export interface ArchitectureElements { 10 | components: Stdlib_C4_Container_Component[]; 11 | relations: Stdlib_C4_Dynamic_Rel[]; 12 | boundaries: ArchitectureBoundary[]; 13 | } 14 | 15 | export interface ArchitectureBoundary { 16 | boundary: Stdlib_C4_Boundary; 17 | couplingRelations: Stdlib_C4_Dynamic_Rel[]; 18 | cohesion: number; 19 | } 20 | 21 | export const groupElements = (elements: UMLElement[]) => { 22 | const result: ArchitectureElements = { 23 | components: [], 24 | boundaries: [], 25 | relations: [], 26 | }; 27 | 28 | for (const element of elements) { 29 | if ( 30 | element instanceof Stdlib_C4_Dynamic_Rel || 31 | element instanceof Relationship 32 | ) { 33 | result.relations.push(element as Stdlib_C4_Dynamic_Rel); 34 | } else if ( 35 | ["System_Boundary", "Container_Boundary", "Boundary"].includes( 36 | (element as Stdlib_C4_Boundary).type_.name, 37 | ) 38 | ) { 39 | result.boundaries.push({ 40 | boundary: element as Stdlib_C4_Boundary, 41 | couplingRelations: [], 42 | cohesion: 0 43 | }); 44 | } else { 45 | result.components.push(element as Stdlib_C4_Container_Component); 46 | } 47 | } 48 | 49 | return result; 50 | }; 51 | -------------------------------------------------------------------------------- /src/deployConfigs/loadMicroserviceDeployConfigs.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | import YAML from "yaml"; 5 | 6 | import { DeployConfig } from "./deployConfig"; 7 | 8 | const getMicroserviceFilepaths = async (): Promise => { 9 | const partialNamesToExclude = ["migrator", "platform", "citest", "tests"]; 10 | const namesToExclude = new Set(["ignore.yml"]); 11 | 12 | const deploysPath = path.join( 13 | process.cwd(), 14 | "resources/kubernetes", 15 | "microservices", 16 | ); 17 | 18 | const filenames = (await fs.readdir(deploysPath, "utf8")) 19 | .filter((filename) => 20 | new Set([".yml", ".yaml"]).has(path.extname(filename)), 21 | ) 22 | .filter((filename) => !namesToExclude.has(filename)) 23 | .filter((filename) => 24 | partialNamesToExclude.every((toExclude) => !filename.includes(toExclude)), 25 | ); 26 | 27 | return filenames.map((filename) => path.join(deploysPath, filename)); 28 | }; 29 | 30 | export const loadMicroserviceDeployConfigs = async (): Promise< 31 | DeployConfig[] 32 | > => { 33 | const filepaths = await getMicroserviceFilepaths(); 34 | return Promise.all( 35 | filepaths.map(async (filePath): Promise => { 36 | const content = await fs.readFile(filePath, "utf8"); 37 | let parsed = YAML.parse(content.replaceAll("env:", "environment:")); 38 | if (parsed.microservice) parsed = parsed.microservice; 39 | parsed.fileName = path.parse(filePath).name; 40 | return parsed; 41 | }), 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/plantuml/lib/filterElements.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Comment, 3 | Relationship, 4 | Stdlib_C4_Boundary, 5 | Stdlib_C4_Container_Component, 6 | Stdlib_C4_Context, 7 | Stdlib_C4_Dynamic_Rel, 8 | UMLElement, 9 | } from "plantuml-parser"; 10 | 11 | export const filterElements = (elements: UMLElement[]): UMLElement[] => { 12 | const result: UMLElement[] = []; 13 | 14 | for (const element of elements) { 15 | if (element instanceof Comment) continue; 16 | if ( 17 | (element as Stdlib_C4_Container_Component).type_.name === "Container" || 18 | (element as Stdlib_C4_Container_Component).type_.name === "ContainerDb" || 19 | (element as Stdlib_C4_Container_Component).type_.name === "Component" || 20 | (element as Stdlib_C4_Context).type_.name === "System_Ext" || 21 | (element as Stdlib_C4_Context).type_.name === "System" || 22 | (element as Stdlib_C4_Context).type_.name === "Person" || 23 | element instanceof Stdlib_C4_Dynamic_Rel || 24 | element instanceof Relationship 25 | ) { 26 | result.push(element); 27 | } 28 | 29 | const elementAsBoundary = element as Stdlib_C4_Boundary; 30 | if ( 31 | ["System_Boundary", "Container_Boundary", "Boundary"].includes( 32 | elementAsBoundary.type_.name, 33 | ) 34 | ) { 35 | result.push(elementAsBoundary); 36 | const resultFromBoundary = filterElements(elementAsBoundary.elements); 37 | result.push(...resultFromBoundary); 38 | } 39 | 40 | if (Array.isArray(element)) { 41 | const resultFromArray = filterElements(element); 42 | result.push(...resultFromArray); 43 | } 44 | } 45 | 46 | return result; 47 | }; 48 | -------------------------------------------------------------------------------- /test/ccr.test.ts: -------------------------------------------------------------------------------- 1 | import { Stdlib_C4_Boundary, Stdlib_C4_Container_Component } from "plantuml-parser"; 2 | 3 | import { analyzeArchitecture } from "../src/analyzer"; 4 | 5 | /** 6 | * Core diagrams https://github.com/plantuml-stdlib/C4-PlantUML/blob/master/samples/C4CoreDiagrams.md 7 | */ 8 | describe("Cascade coupling reduction", () => { 9 | it("test1", async () => { 10 | // const L2Report = await analyzeArchitecture("banking/C4L2.puml"); 11 | // const L3Report = await analyzeArchitecture("banking/C4L3.puml"); 12 | 13 | const BoundariesReport = await analyzeArchitecture("boundaries.puml"); 14 | 15 | // console.log(L2Report, L3Report, BoundariesReport); 16 | 17 | for (const b of BoundariesReport.elements.boundaries) { 18 | console.log(b.boundary.label, `, cohesion: ${b.cohesion}`, `, coupling: ${b.couplingRelations.length}`); 19 | 20 | const parentBoundary = BoundariesReport.elements.boundaries 21 | .find(pb=>pb.boundary.elements.some(e=>(e as Stdlib_C4_Boundary).alias === b.boundary.alias)); 22 | 23 | if(parentBoundary) 24 | { 25 | const parentCoupling = parentBoundary.couplingRelations 26 | .filter(r => b.boundary.elements.some(e => (e as Stdlib_C4_Container_Component).alias === r.from)).length; 27 | 28 | expect(b.cohesion).toBeGreaterThanOrEqual(b.couplingRelations.length); 29 | expect(b.couplingRelations.length).toBeGreaterThanOrEqual(parentCoupling); 30 | 31 | console.log(`${b.cohesion} ≥ ${b.couplingRelations.length} ≥ ${parentCoupling}`); 32 | } 33 | } 34 | 35 | // console.log(BoundariesReport); 36 | // console.log(BoundariesReport.elements.boundaries[0]); 37 | 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /resources/architecture/generated.puml: -------------------------------------------------------------------------------- 1 | @startuml "Demo Generated" 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 3 | LAYOUT_WITH_LEGEND() 4 | AddRelTag("async", $lineStyle = DottedLine()) 5 | AddElementTag("acl", $bgColor = "#6F9355") 6 | Boundary(project, "Our system"){ 7 | Container(bff, "bff") 8 | Container(camunda, "camunda") 9 | Container(goods_acl, "goods acl", "", "", $tags="acl") 10 | Container(invoice_acl, "invoice acl", "", "", $tags="acl") 11 | Container(invoice_repository, "invoice repository") 12 | ContainerDb(invoice_repository_db, "DB") 13 | Rel(invoice_repository, invoice_repository_db, "") 14 | Container(stock_acl, "stock acl", "", "", $tags="acl") 15 | Container(task_repository, "task repository") 16 | ContainerDb(task_repository_db, "DB") 17 | Rel(task_repository, task_repository_db, "") 18 | } 19 | Rel(bff, invoice_repository, "") 20 | Rel(bff, task_repository, "") 21 | Rel(bff, camunda, "") 22 | Rel(bff, goods_acl, "") 23 | Rel(camunda, invoice_repository, "") 24 | Rel(camunda, task_repository, "") 25 | Rel(camunda, stock_acl, "") 26 | Rel(camunda, invoice_acl, "") 27 | System_Ext(goods_repository, "goods_repository", " ") 28 | Rel(goods_acl, goods_repository, "", "https://gateway.int.com:443/goods/v1") 29 | System_Ext(goods2_repository, "goods2_repository", " ") 30 | Rel(goods_acl, goods2_repository, "", "https://gateway.int.com:443/goods2/v1") 31 | System_Ext(ext_system, "ext_system", " ") 32 | Rel(invoice_acl, ext_system, "", "https://gateway.int.com:443/ext/v1") 33 | System_Ext(invoice_input, "invoice_input", " ") 34 | Rel(invoice_acl, invoice_input, "", "orig-invoice-q8s-v1", $tags="async") 35 | System_Ext(invoice_output, "invoice_output", " ") 36 | Rel(invoice_acl, invoice_output, "", "result-invoice-q8s-v1", $tags="async") 37 | System_Ext(stock, "stock", " ") 38 | Rel(stock_acl, stock, "", "https://gateway.int.com:443/stock/v1") 39 | @enduml -------------------------------------------------------------------------------- /resources/architecture/banking/C4L3.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml 3 | ' uncomment the following line and comment the first to use locally 4 | ' !include C4_Component.puml 5 | 6 | LAYOUT_WITH_LEGEND() 7 | 8 | title Component diagram for Internet Banking System - API Application 9 | 10 | Container(spa, "Single Page Application", "javascript and angular", "Provides all the internet banking functionality to customers via their web browser.") 11 | Container(ma, "Mobile App", "Xamarin", "Provides a limited subset ot the internet banking functionality to customers via their mobile mobile device.") 12 | ContainerDb(db, "Database", "Relational Database Schema", "Stores user registration information, hashed authentication credentials, access logs, etc.") 13 | System_Ext(mbs, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") 14 | 15 | Container_Boundary(api, "API Application") { 16 | Component(sign, "Sign In Controller", "MVC Rest Controller", "Allows users to sign in to the internet banking system") 17 | Component(accounts, "Accounts Summary Controller", "MVC Rest Controller", "Provides customers with a summary of their bank accounts") 18 | Component(security, "Security Component", "Spring Bean", "Provides functionality related to singing in, changing passwords, etc.") 19 | Component(mbsfacade, "Mainframe Banking System Facade", "Spring Bean", "A facade onto the mainframe banking system.") 20 | 21 | Rel(sign, security, "Uses") 22 | Rel(accounts, mbsfacade, "Uses") 23 | Rel(security, db, "Read & write to", "JDBC") 24 | Rel(mbsfacade, mbs, "Uses", "XML/HTTPS") 25 | } 26 | 27 | Rel(spa, sign, "Uses", "JSON/HTTPS") 28 | Rel(spa, accounts, "Uses", "JSON/HTTPS") 29 | 30 | Rel(ma, sign, "Uses", "JSON/HTTPS") 31 | Rel(ma, accounts, "Uses", "JSON/HTTPS") 32 | @enduml 33 | -------------------------------------------------------------------------------- /patterns.md: -------------------------------------------------------------------------------- 1 | | Паттерн | Описание | ADR | Пример теста | 2 | | ------------- | ------------- | ------------- | ------------- | 3 | | Anti-corruption Layer | За интеграцию с внешними системами ответственны отдельные микросервисы-адаптеры, инкапсулирующие в себе знания о внешних системах| [ADR](https://github.com/Byndyusoft/aact/blob/main/ADRs/Anti-corruption%20Layer.md) | [тест](https://github.com/Byndyusoft/aact/blob/workshop/test/acl.test.ts) | 4 | | Пассивные CRUD-сервисы | Микросервисы, отвечающие за доступ к мастер-данным, не обладают дополнительной логикой и не имеют зависимостей помимо своей БД | [ADR](https://github.com/Byndyusoft/aact/blob/main/ADRs/Database%20per%20CRUD-service.md) | [тест](https://github.com/Byndyusoft/aact/blob/workshop/test/crud.test.ts) | 5 | | API-Gateway | Внешние для периметра REST-вызовы не должны идти напрямую | | [тест](https://github.com/Byndyusoft/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L127C16-L127C16) | 6 | | Оркестратор распределённых транзакций | Запись данных идёт только через оркестратор бизнес-процессов, чтение доступно напрямую | | | 7 | | Database per service | К БД имеет доступ только один микросервис | [ADR](https://github.com/Byndyusoft/aact/blob/main/ADRs/Database%20per%20CRUD-service.md) | [тест](https://github.com/Byndyusoft/aact/blob/workshop/test/crud.test.ts) | 8 | | Stable Dependencies Principle | Зависимости направлены от менее стабильных микросервисов к более стабильным | | | 9 | | Acyclic Dependencies Principle | Граф зависимостей не должен содержать циклов | | [тест](https://github.com/Byndyusoft/aact/blob/workshop/test/acyclic.test.ts) | 10 | | Common Reuse Principle | Зависимость от контекста микросервисов использует внешнеконтекстные API всех микросервисов контекста | | | 11 | | ... | | | | 12 | | ⚠️ | **Добавляйте свои примеры, вместе подумаем, как их покрыть тестами** | | | 13 | -------------------------------------------------------------------------------- /ModularMonolith/Monolith.Api/Monolith.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/deployConfigs/mapContainersFromDeployConfigs.ts: -------------------------------------------------------------------------------- 1 | import { Section } from "../entities"; 2 | 3 | import { DeployConfig } from "./index"; 4 | 5 | const mapFromConfig = (deployConfig: DeployConfig): DeployConfig => { 6 | const envWhitelist = [ 7 | "BASE_URL", 8 | "PROTOCOL", 9 | /[A-Z_]+(?([]); 25 | 26 | const environment = deployConfig?.environment ?? {}; 27 | const envKeys = Object.keys(environment); 28 | const filteredEnvKeys = envKeys.filter((envName) => 29 | envWhitelist.some((white) => envName.match(white)), 30 | ); 31 | 32 | const sections: Section[] = filteredEnvKeys 33 | .map((envName) => { 34 | const value = environment[envName] as any; 35 | return { 36 | prod_value: value.prod ?? value.default, 37 | name: envNamePartsToCleanup 38 | .reduce( 39 | (acc, partToCleanup) => acc.toString().replace(partToCleanup, ""), 40 | envName, 41 | ) 42 | .toString(), 43 | }; 44 | }) 45 | .map((relation) => { 46 | relation.name = relation.name.toLowerCase(); 47 | for (const entry of synonymes.entries()) { 48 | if (entry[1].includes(relation.name)) relation.name = entry[0]; 49 | } 50 | return relation; 51 | }); 52 | deployConfig.name = (deployConfig.name ?? deployConfig.fileName).replace( 53 | /[\s-\(\)]/g, 54 | "_", 55 | ); 56 | deployConfig.sections = sections; 57 | 58 | return deployConfig; 59 | }; 60 | 61 | export const mapFromConfigs = ( 62 | deployConfigs: DeployConfig[], 63 | ): DeployConfig[] => { 64 | return deployConfigs 65 | .map(mapFromConfig) 66 | .sort((a, b) => a.name.localeCompare(b.name)); 67 | }; 68 | -------------------------------------------------------------------------------- /resources/architecture/banking/C4L2.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 3 | ' uncomment the following line and comment the first to use locally 4 | ' !include C4_Container.puml 5 | 6 | ' LAYOUT_TOP_DOWN() 7 | ' LAYOUT_AS_SKETCH() 8 | LAYOUT_WITH_LEGEND() 9 | 10 | title Container diagram for Internet Banking System 11 | 12 | Person(customer, Customer, "A customer of the bank, with personal bank accounts") 13 | 14 | System_Boundary(c1, "Internet Banking") { 15 | Container(web_app, "Web Application", "Java, Spring MVC", "Delivers the static content and the Internet banking SPA") 16 | Container(spa, "Single-Page App", "JavaScript, Angular", "Provides all the Internet banking functionality to customers via their web browser") 17 | Container(mobile_app, "Mobile App", "C#, Xamarin", "Provides a limited subset of the Internet banking functionality to customers via their mobile device") 18 | ContainerDb(database, "Database", "SQL Database", "Stores user registration information, hashed auth credentials, access logs, etc.") 19 | Container(backend_api, "API Application", "Java, Docker Container", "Provides Internet banking functionality via API") 20 | } 21 | 22 | System_Ext(email_system, "E-Mail System", "The internal Microsoft Exchange system") 23 | System_Ext(banking_system, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.") 24 | 25 | Rel(customer, web_app, "Uses", "HTTPS") 26 | Rel(customer, spa, "Uses", "HTTPS") 27 | Rel(customer, mobile_app, "Uses") 28 | 29 | Rel_Neighbor(web_app, spa, "Delivers") 30 | Rel(spa, backend_api, "Uses", "async, JSON/HTTPS") 31 | Rel(mobile_app, backend_api, "Uses", "async, JSON/HTTPS") 32 | Rel_Back_Neighbor(database, backend_api, "Reads from and writes to", "sync, JDBC") 33 | 34 | Rel_Back(customer, email_system, "Sends e-mails to") 35 | Rel_Back(email_system, backend_api, "Sends e-mails using", "sync, SMTP") 36 | Rel_Neighbor(backend_api, banking_system, "Uses", "sync/async, XML/HTTPS") 37 | @enduml 38 | -------------------------------------------------------------------------------- /ADRs/Database per CRUD-service.md: -------------------------------------------------------------------------------- 1 | # Каждая БД закрыта CRUD-микросервисом, не содержащим бизнес-логики 2 | 3 | ## Контекст 4 | 5 | Микросервисная архитектура с выделенными [мастер-системами](https://byndyusoft.com/blogs/masterdata), данные каждой из которых хранятся в отдельной БД 6 | 7 | ## Краткое описание решения и его обоснование 8 | ### Схема 9 | ![image](https://github.com/Byndyusoft/aact/assets/1096954/70b0a5e2-e78c-41c3-bd3c-7d12303ca31a) 10 | 11 | ### Решение 12 | 13 | Выделение отдельных микросервисов, предоставляющих по API возможность CRUD-операций над данными. Доступ к каждой БД есть только у соответствующего CRUD-сервиса. 14 | 15 | ### Обоснование 16 | 17 | Прямой доступ и всё разнообразие возможных операций (SQL-запросов) к БД должно быть ограничено стандартизованным API. Что позволяет избежать различные запросы на получение одних и тех же данных или выполнение схожих команд. Ограничение выполняемых SQL-запросов позволяет под них оптимизировать индексы и другие механизмы БД, что было бы менее эффективно при разнородных запросах от многих потребителей. 18 | Отсутствие бизнес-логики в CRUD-сервисах позволяет соблюсти принципы Single Responsibility и Common Closure принципов, разделить логику от данных. 19 | 20 | ## Как покрыть тестами 21 | 22 | 1. Помечаем на архитектуре соответствующие микросервисы признаком "CRUD" (или Repository) 23 | ``` 24 | Container(task_repository, “Task Repository", "NestJS", $tags="crud") 25 | ``` 26 | 2. Проверяем, что у таких сервисов нет исходящих связей, кроме единственной БД 27 | ``` 28 | Rel(task_repository, task_db, "") — ✅ т.к. исходящая связь в БД 29 | Rel(camunda, task_repository, "") — ✅ т.к. входящая связь 30 | Rel(task_repository, invoice_acl, "") — ❎ т.к. исходящая связь не в БД 31 | ``` 32 | 3. Проверяем, что у БД нет никаких связей помимо единственной с crud-микросервисом 33 | ``` 34 | Rel(task_repository, task_db, "") — ✅ т.к. микросервис task_repository помечен тэгом "crud" 35 | Rel(invoice_acl, task_db, "") — ❎ т.к. микросервис invoice_acl непомечен тэгом "crud" 36 | Rel(invoice_repository, task_db, "") — ❎ т.к. выше уже есть одна связь у данной БД, хоть и invoice_repository помечен тэгом "crud" 37 | ``` 38 | 39 | ### Примеры тестов 40 | TBD 41 | -------------------------------------------------------------------------------- /resources/architecture/C4L2.puml: -------------------------------------------------------------------------------- 1 | @startuml "Demo Tests" 2 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 3 | 4 | LAYOUT_WITH_LEGEND() 5 | 6 | AddRelTag("async", $lineStyle = DottedLine()) 7 | AddElementTag("acl", $bgColor = "#6F9355") 8 | 9 | System_Ext(stock, "Stock") 10 | System_Ext(ext_system, "Ext System") 11 | System_Ext(invoice_source, "Invoice Source") 12 | System_Ext(invoice_dest, "Invoice Consumer") 13 | System_Ext(goods_repository, "Goods Repository") 14 | System_Ext(goods2_repository, "Goods2 Repository") 15 | System_Ext(front, "Front") 16 | 17 | Boundary(our_system, "Our system"){ 18 | Container(bff, "BFF") 19 | Container(goods_acl, "Goods ACL", "", "", $tags="acl") 20 | Container(camunda, "Camunda") 21 | Container(task_repository, "Task Repository", "", "", $tags="repo") 22 | Container(invoice_repository, "Invoice Repository", "", "", $tags="repo") 23 | Container(invoice_acl, "Invoice ACL", "", "", $tags="acl") 24 | Container(stock_acl, "Stock ACL", "", "", $tags="acl") 25 | 26 | ContainerDb(task_repository_db,"DB") 27 | ContainerDb(invoice_repository_db,"DB") 28 | 29 | 'Container(invoice_relay, "Invoice Relay", "", "", $tags="relay") 30 | } 31 | 32 | Rel(front, bff, "") 33 | Rel(bff, camunda, "") 34 | Rel(camunda, stock_acl, "") 35 | Rel(stock_acl, stock, "", "https://gateway.int.com:443/stock/v1") 36 | 37 | Rel(bff, goods_acl, "") 38 | Rel(goods_acl, goods_repository, "", "https://gateway.int.com:443/goods/v1") 39 | Rel(goods_acl, goods2_repository, "", "https://gateway.int.com:443/goods2/v1") 40 | 41 | Rel(bff, task_repository, "") 42 | Rel(task_repository, task_repository_db, "") 43 | 'Rel(task_repository, bff, "") 44 | 45 | Rel(bff, invoice_repository, "") 46 | Rel(invoice_repository, invoice_repository_db, "") 47 | 'Rel(invoice_relay, invoice_repository_db, "") 48 | 49 | Rel(camunda, task_repository, "") 50 | Rel(camunda, invoice_repository, "") 51 | Rel(camunda, invoice_acl, "") 52 | Rel(invoice_source, invoice_acl, "", "orig-invoice-q8s-v1", $tags="async") 53 | Rel(invoice_acl, invoice_dest, "", "result-invoice-q8s-v1", $tags="async") 54 | Rel(invoice_acl, ext_system, "", "https://gateway.int.com:443/ext/v1") 55 | 'Rel(invoice_acl, bff, "", "https://gateway.int.com:443/ext/v1") 56 | 57 | @enduml 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ArchAsCode_Tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Deployment information", 6 | "homepage": "https://github.com/razonrus/ArchAsCode_Tests#readme", 7 | "bugs": { 8 | "url": "https://github.com/razonrus/ArchAsCode_Tests/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/razonrus/ArchAsCode_Tests.git" 13 | }, 14 | "license": "ISC", 15 | "author": "Ruslan Safin (Byndyusoft)", 16 | "scripts": { 17 | "test": "jest", 18 | "lint": "npm run lint:eslint && npm run lint:markdown && npm run lint:prettier", 19 | "lint:eslint": "eslint --ignore-path ./.gitignore --max-warnings 0 --ext .ts,.js .", 20 | "lint:markdown": "markdownlint --ignore-path ./.gitignore \"./**/*.md\"", 21 | "lint:prettier": "prettier --ignore-path ./.gitignore --check \"./**/*.{ts,js,json,yaml,yml,md}\"" 22 | }, 23 | "jest": { 24 | "reporters": [ 25 | "default", 26 | [ 27 | "github-actions", 28 | { 29 | "silent": false 30 | } 31 | ], 32 | "summary" 33 | ], 34 | "moduleFileExtensions": [ 35 | "ts", 36 | "js" 37 | ], 38 | "resetMocks": true, 39 | "rootDir": "./", 40 | "roots": [ 41 | "/test" 42 | ], 43 | "setupFilesAfterEnv": [ 44 | "jest-extended/all" 45 | ], 46 | "testEnvironment": "node", 47 | "testRegex": ".*\\.test\\.ts$", 48 | "transform": { 49 | "^.+\\.ts$": "ts-jest" 50 | } 51 | }, 52 | "devDependencies": { 53 | "@byndyusoft/eslint-config": "2.4.0", 54 | "@byndyusoft/tsconfig": "1.2.0", 55 | "@commitlint/cli": "17.0.3", 56 | "@commitlint/config-conventional": "17.0.3", 57 | "@types/jest": "29.5.2", 58 | "eslint": "8.19.0", 59 | "husky": "8.0.1", 60 | "jest": "29.5.0", 61 | "jest-extended": "4.0.0", 62 | "jest-junit": "16.0.0", 63 | "lint-staged": "13.0.3", 64 | "markdownlint-cli": "0.31.1", 65 | "plantuml-parser": "0.4.0", 66 | "prettier": "2.7.1", 67 | "prettier-plugin-packagejson": "2.2.18", 68 | "ts-jest": "29.1.0", 69 | "ts-patch": "2.0.1", 70 | "tslib": "2.5.3", 71 | "typescript": "5.1.3", 72 | "yaml": "2.3.1" 73 | }, 74 | "engines": { 75 | "node": ">=16" 76 | }, 77 | "dependencies": { 78 | "yarn": "^1.22.19" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ModularMonolith/Monolith.ArchitectureTests/Modules/Module.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Monolith.ArchitectureTests.Modules 4 | { 5 | /// 6 | /// Описание модуля 7 | /// 8 | public class Module 9 | { 10 | private readonly string _contract = default!; 11 | private readonly string[] _implementation = Array.Empty(); 12 | private readonly (string, string)[] _internalReferences = Array.Empty<(string, string)>(); 13 | 14 | /// 15 | /// Название модуля 16 | /// 17 | public string Name { get; init; } = default!; 18 | 19 | /// 20 | /// Список модулей от которых зависит этот модуль 21 | /// 22 | public List ReferencedModules { get; init; } = new(); 23 | 24 | /// 25 | /// Алисы сборок в формате {алиас, сборка}. 26 | /// Если алиасы не пустые, то для заполнения контракта, реализации и внутренних ссылок должны использовать алиасы 27 | /// 28 | public Dictionary UseAssemblyAliases { get; init; } = new(); 29 | 30 | /// 31 | /// Название контрактной сборки 32 | /// 33 | public string Contract 34 | { 35 | get => _contract; 36 | init => _contract = UseAssemblyAliases.Any() 37 | ? UseAssemblyAliases[value] 38 | : value; 39 | } 40 | 41 | /// 42 | /// Название сборок реализации модуля 43 | /// 44 | public string[] Implementation 45 | { 46 | get => _implementation; 47 | init => _implementation = UseAssemblyAliases.Any() 48 | ? value.Select(x => UseAssemblyAliases[x]).ToArray() 49 | : value; 50 | } 51 | 52 | /// 53 | /// Связи между сборками внутри модуля в формате (сборка, подключаемая сборка) 54 | /// 55 | public (string, string)[] InternalReferences 56 | { 57 | get => _internalReferences; 58 | init => _internalReferences = UseAssemblyAliases.Any() 59 | ? value.Select(x => (UseAssemblyAliases[x.Item1], UseAssemblyAliases[x.Item2])).ToArray() 60 | : value; 61 | } 62 | 63 | /// 64 | /// Получить все сборки модуля 65 | /// 66 | public IEnumerable AllAssemblies => Implementation.Append(Contract); 67 | 68 | public override string ToString() 69 | { 70 | return Name; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/boundaries.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { Boundary, PumlFile } from "../src/entities"; 4 | 5 | import { 6 | loadPlantumlElements, 7 | mapContainersFromPlantumlElements, 8 | } from "../src/plantuml"; 9 | 10 | const SystemExternalType = "System_Ext"; 11 | const ContainerType = "Container"; 12 | 13 | describe("Architecture", () => { 14 | let pumlFile: PumlFile; 15 | 16 | beforeAll(async () => { 17 | const pumlElements = await loadPlantumlElements("boundaries.puml"); 18 | pumlFile = mapContainersFromPlantumlElements(pumlElements); 19 | }); 20 | 21 | beforeEach(() => { 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, n/global-require 23 | global.console = require("console"); 24 | }); 25 | 26 | afterEach(() => { 27 | global.console = console; 28 | }); 29 | 30 | it("check coupling and cohesion", () => { 31 | // для всех наших контекстов и периметров 32 | for (const boundary of pumlFile.boundaries) { 33 | const cohesion = GetBoundaryCohesion(boundary); 34 | const coupling = GetBoundaryCoupling(boundary); 35 | 36 | console.log( 37 | boundary.label + " — Cohesion: " + cohesion + "; Coupling: " + coupling, 38 | ); 39 | // во-первых, внутренняя прочность периметра должна быть больше внешней связанности 40 | expect(cohesion).toBeGreaterThan(coupling); 41 | 42 | // во-вторых, если у периметр содержит в себе другие периметры — его прочность должна быть меньше суммы прочностей внутренних периметров 43 | if (boundary.boundaries.length > 0) 44 | expect(cohesion).toBeLessThan( 45 | boundary.boundaries.reduce( 46 | (sum: number, current: Boundary) => 47 | sum + GetBoundaryCohesion(current), 48 | 0, 49 | ), 50 | ); 51 | } 52 | }); 53 | }); 54 | 55 | function GetBoundaryCohesion(boundary: Boundary) { 56 | var result = 0; 57 | for (const container of boundary.containers) { 58 | result += container.relations.filter((r) => 59 | boundary.containers.some((c) => c.name == r.to.name), 60 | ).length; 61 | } 62 | for (const innerBoundary of boundary.boundaries) { 63 | result += GetBoundaryCoupling(innerBoundary); 64 | } 65 | return result; 66 | } 67 | 68 | function GetBoundaryCoupling(boundary: Boundary) { 69 | var result = 0; 70 | 71 | for (const container of boundary.containers) { 72 | result += container.relations.filter( 73 | (r) => 74 | r.to.type == ContainerType && 75 | !boundary.containers.some((c) => c.name == r.to.name), 76 | ).length; 77 | } 78 | 79 | for (const innerBoundary of boundary.boundaries) { 80 | for (const container of innerBoundary.containers) { 81 | result += container.relations.filter( 82 | (r) => r.to.type == SystemExternalType, 83 | ).length; 84 | } 85 | } 86 | 87 | return result; 88 | } 89 | -------------------------------------------------------------------------------- /src/plantuml/mapContainersFromPlantumlElements.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Stdlib_C4_Boundary, 3 | Stdlib_C4_Container_Component, 4 | Stdlib_C4_Context, 5 | Stdlib_C4_Dynamic_Rel, 6 | UMLElement, 7 | } from "plantuml-parser"; 8 | 9 | import { Boundary,Container, PumlFile } from "../entities"; 10 | 11 | const addDependency = ( 12 | containers: Container[], 13 | relation: Stdlib_C4_Dynamic_Rel, 14 | ): void => { 15 | const containerFrom = containers.find((x) => x.name === relation.from); 16 | if (!containerFrom) return; 17 | const containerTo = containers.find((x) => x.name === relation.to); 18 | if (!containerTo) return; 19 | containerFrom.relations.push({ 20 | to: containerTo, 21 | technology: relation.techn, 22 | tags: relation.descr?.split(",").map((t) => t.trim()), 23 | }); 24 | }; 25 | 26 | export const mapContainersFromPlantumlElements = ( 27 | elements: UMLElement[], 28 | ): PumlFile => { 29 | const containers: Container[] = elements 30 | .filter( 31 | (element) => 32 | element instanceof Stdlib_C4_Container_Component || 33 | element instanceof Stdlib_C4_Context, 34 | ) 35 | .map((element) => { 36 | const component = element as Stdlib_C4_Container_Component; 37 | return { 38 | name: component.alias, 39 | label: component.label, 40 | type: component.type_.name, 41 | relations: [], 42 | tags: component.sprite, 43 | description: component.descr, 44 | }; 45 | }); 46 | 47 | for (const element of elements) { 48 | if (element instanceof Stdlib_C4_Container_Component) { 49 | continue; 50 | } 51 | 52 | if (element instanceof Stdlib_C4_Dynamic_Rel) { 53 | addDependency(containers, element); 54 | } 55 | } 56 | 57 | const boundaries: Boundary[] = elements 58 | .filter((element) => element instanceof Stdlib_C4_Boundary) 59 | .map((element) => { 60 | const component = element as Stdlib_C4_Boundary; 61 | return { 62 | name: component.alias, 63 | label: component.label, 64 | type: component.type_.name, 65 | boundaries: [], 66 | containers: containers.filter((container) => 67 | component.elements 68 | .filter( 69 | (element) => element instanceof Stdlib_C4_Container_Component, 70 | ) 71 | .some( 72 | (e) => 73 | (e as Stdlib_C4_Container_Component).alias == container.name, 74 | ), 75 | ), 76 | }; 77 | }); 78 | 79 | for (const boundary of boundaries) { 80 | var component = elements.filter( 81 | (element) => 82 | element instanceof Stdlib_C4_Boundary && 83 | (element as Stdlib_C4_Boundary).alias == boundary.name, 84 | )[0] as Stdlib_C4_Boundary; 85 | 86 | boundary.boundaries = boundaries.filter((b) => 87 | component.elements 88 | .filter((element) => element instanceof Stdlib_C4_Boundary) 89 | .some((e) => (e as Stdlib_C4_Boundary).alias == b.name), 90 | ); 91 | } 92 | 93 | return { 94 | allContainers: containers.sort((a, b) => a.name.localeCompare(b.name)), 95 | boundaries: boundaries, 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /ModularMonolith/Readme.md: -------------------------------------------------------------------------------- 1 | # Тестирование архитектуры модульного монолита на C# 2 | 3 | Архитектура модульного монолита описывается правилами организации и взаимодействия модулей. 4 | 5 | Имея описание целевой архитектуры можно проверить, соответствует ли реальная архитектура целевой. 6 | 7 | ## Модули 8 | 9 | Модуль - это несколько сборок объединённых одним названием. Обычно помещаются в отдельную папку. 10 | 11 | Для модулей действуют правила, каждое из которых можно проверить: 12 | * Каждый модуль имеет контрактную сборку и, возможно, сборки с реализацией. 13 | * Одни модули могут использовать другие модули, но только через контрактную сборку. Доступ к другим сборкам модуля из других модулей запрещён. 14 | * Зависимости между модулями должны образовывать ориентированный граф без циклов. 15 | * Сборки модулей должны подключить только модули, которые есть в целевой архитектуре 16 | 17 | ## Реализация тестов 18 | 19 | Текущее решение демонстрирует как можно декларативно описать связи между модулями в виде кода и тестом проверить, что реальные связи между сборками соответствуют этому описанию. 20 | 21 | Тесты не имеют зависимостей и могут запускаться на CI/CD как обычные юнит тесты. 22 | 23 | .net нюанс - если подключить к проекту другой проект, но не использовать из него ни один тип, то в скомпилированной сборке не будет ссылки на сборку из другого проекта. 24 | 25 | Пример описания 26 | 27 | ```C# 28 | new[] 29 | { 30 | new Module 31 | { 32 | Name = "Common", 33 | Contract = "Common.Contracts", 34 | Implementation = ["Common.Domain"], 35 | InternalReferences = [ 36 | ("Common.Domain", "Common.Contracts") 37 | ] 38 | }, 39 | new Module 40 | { 41 | Name = "Emails", 42 | ReferencedModules = { "Common" }, 43 | Contract = "Emails.Contracts", 44 | Implementation = ["Emails.Domain"], 45 | InternalReferences = [ 46 | ("Emails.Domain", "Emails.Contracts") 47 | ] 48 | }, 49 | new Module 50 | { 51 | Name = "Audit", 52 | ReferencedModules = { "Common" }, 53 | Contract = "Audit.Contracts", 54 | Implementation = ["Audit.Domain"], 55 | InternalReferences = [ 56 | ("Audit.Domain", "Audit.Contracts") 57 | ] 58 | }, 59 | new Module 60 | { 61 | Name = "Shedule", 62 | ReferencedModules = { "Common", "Emails", "Audit" }, 63 | Contract = "Shedule.Contract", 64 | Implementation = ["Shedule.Domain", "Shedule.DataAccess"], 65 | InternalReferences = [ 66 | ("Shedule.Domain", "Shedule.Contract"), 67 | ("Shedule.DataAccess", "Shedule.Domain") 68 | ] 69 | }, 70 | //И т.д. 71 | 72 | }; 73 | ``` 74 | 75 | Из описания можно построить диаграммы зависимостей модулей. 76 | 77 | Модули: 78 | 79 | 80 | Модули и сборки: 81 | -------------------------------------------------------------------------------- /src/analyzer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prevent-abbreviations */ 2 | 3 | import { Stdlib_C4_Boundary, Stdlib_C4_Container_Component } from "plantuml-parser"; 4 | 5 | import { 6 | ArchitectureElements, 7 | groupElements, 8 | } from "./plantuml/lib/groupElements"; 9 | import { loadPlantumlElements } from "./plantuml"; 10 | 11 | interface AnalyzedArchitecture { 12 | elements: ArchitectureElements; 13 | report: AnalysisReport; 14 | } 15 | 16 | interface AnalysisReport { 17 | elementsCount: number; 18 | syncApiCalls: number; 19 | asyncApiCalls: number; 20 | databases: DatabasesInfo; 21 | } 22 | 23 | interface DatabasesInfo { 24 | count: number; 25 | consumes: number; 26 | } 27 | 28 | const apiTechnologies = ["http", "grpc", "tcp"]; 29 | 30 | const analyzeElements = (elements: ArchitectureElements): AnalysisReport => { 31 | const asyncApiCalls = elements.relations.filter((it) => 32 | (it.descr ?? "").includes("async"), 33 | ); 34 | const syncApiCalls = elements.relations.filter((it) => { 35 | const component = elements.components.find((ct) => ct.alias === it.to); 36 | const isExternalApi = (component!.type_.name as string) === "System_Ext"; 37 | const isApiTechnology = apiTechnologies.some((apiTechn) => 38 | (it.techn ?? "").toLowerCase().includes(apiTechn), 39 | ); 40 | 41 | return it.descr !== "async" && (isExternalApi || isApiTechnology); 42 | }); 43 | 44 | for (const archBoundary of elements.boundaries) { 45 | const parentBoundary = elements.boundaries 46 | .find(b=>b.boundary.elements.some(e=>(e as Stdlib_C4_Boundary).alias === archBoundary.boundary.alias)); 47 | 48 | for (const relation of elements.relations) { 49 | if(archBoundary.boundary.elements.some(e=>(e as Stdlib_C4_Container_Component).alias === relation.from)) 50 | { 51 | if(archBoundary.boundary.elements.some(e=>(e as Stdlib_C4_Container_Component).alias === relation.to)) 52 | archBoundary.cohesion++; 53 | else 54 | { 55 | if(!parentBoundary || parentBoundary.boundary.elements.some(b=>(b as Stdlib_C4_Boundary) 56 | .elements.some(e=>(e as Stdlib_C4_Container_Component).alias === relation.to))) 57 | { 58 | archBoundary.couplingRelations.push(relation); 59 | if(parentBoundary) { 60 | parentBoundary.cohesion++; 61 | } 62 | } 63 | else 64 | parentBoundary.couplingRelations.push(relation); 65 | } 66 | } 67 | } 68 | } 69 | 70 | return { 71 | elementsCount: elements.components.length, 72 | syncApiCalls: syncApiCalls.length, 73 | asyncApiCalls: asyncApiCalls.length, 74 | databases: analyzeDatabases(elements) 75 | }; 76 | }; 77 | 78 | const analyzeDatabases = (elements: ArchitectureElements): DatabasesInfo => { 79 | const dbContainers = elements.components.filter( 80 | (it) => it.type_.name === "ContainerDb", 81 | ); 82 | 83 | const dbRelations = elements.relations.filter((it) => 84 | dbContainers.some((ct) => [it.from, it.to].includes(ct.alias)), 85 | ); 86 | 87 | return { 88 | count: dbContainers.length, 89 | consumes: dbRelations.length, 90 | }; 91 | }; 92 | 93 | export const analyzeArchitecture = async ( 94 | filename: string, 95 | ): Promise => { 96 | const pumlElements = await loadPlantumlElements(filename); 97 | const groupedElements = groupElements(pumlElements); 98 | 99 | return { 100 | elements: groupedElements, 101 | report: analyzeElements(groupedElements), 102 | }; 103 | }; 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### other workspaces 2 | 3 | **/.scannerwork 4 | 5 | ### https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 6 | 7 | .idea/* 8 | 9 | ### https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 10 | 11 | .vscode/* 12 | *.code-workspace 13 | 14 | # Local History for Visual Studio Code 15 | .history/ 16 | 17 | ### https://github.com/github/gitignore/blob/master/Node.gitignore 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | lerna-debug.log* 26 | 27 | # Diagnostic reports (https://nodejs.org/api/report.html) 28 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 29 | # Test report 30 | reports/* 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | web_modules/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # npmrc config 74 | .npmrc 75 | 76 | # Optional eslint cache 77 | .eslintcache 78 | 79 | # Microbundle cache 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | *.tgz 90 | 91 | # Yarn Integrity file 92 | .yarn-integrity 93 | 94 | # dotenv environment variables file 95 | .env 96 | .env.test 97 | 98 | # parcel-bundler cache (https://parceljs.org/) 99 | .cache 100 | .parcel-cache 101 | 102 | # Next.js build output 103 | .next 104 | out 105 | 106 | # Nuxt.js build / generate output 107 | .nuxt 108 | dist 109 | 110 | # Gatsby files 111 | .cache/ 112 | # Comment in the public line in if your project uses Gatsby and not Next.js 113 | # https://nextjs.org/blog/next-9-1#public-directory-support 114 | # public 115 | 116 | # vuepress build output 117 | .vuepress/dist 118 | 119 | # Serverless directories 120 | .serverless/ 121 | 122 | # FuseBox cache 123 | .fusebox/ 124 | 125 | # DynamoDB Local files 126 | .dynamodb/ 127 | 128 | # TernJS port file 129 | .tern-port 130 | 131 | # Stores VSCode versions used for testing VSCode extensions 132 | .vscode-test 133 | 134 | # yarn v2 135 | .yarn/cache 136 | .yarn/unplugged 137 | .yarn/build-state.yml 138 | .yarn/install-state.gz 139 | .pnp.* 140 | 141 | ### https://github.com/github/gitignore/blob/master/Global/Linux.gitignore 142 | 143 | *~ 144 | 145 | # temporary files which can be created if a process still has a handle open of a deleted file 146 | .fuse_hidden* 147 | 148 | # KDE directory preferences 149 | .directory 150 | 151 | # Linux trash folder which might appear on any partition or disk 152 | .Trash-* 153 | 154 | # .nfs files are created when an open file is removed but is still being accessed 155 | .nfs* 156 | 157 | ### https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 158 | 159 | # Windows thumbnail cache files 160 | Thumbs.db 161 | Thumbs.db:encryptable 162 | ehthumbs.db 163 | ehthumbs_vista.db 164 | 165 | # Dump file 166 | *.stackdump 167 | 168 | # Folder config file 169 | [Dd]esktop.ini 170 | 171 | # Recycle Bin used on file shares 172 | $RECYCLE.BIN/ 173 | 174 | # Windows Installer files 175 | *.cab 176 | *.msi 177 | *.msix 178 | *.msm 179 | *.msp 180 | 181 | # Windows shortcuts 182 | *.lnk 183 | 184 | ### https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 185 | 186 | # General 187 | .DS_Store 188 | .AppleDouble 189 | .LSOverride 190 | 191 | # Icon must end with two \r 192 | Icon 193 | 194 | 195 | # Thumbnails 196 | ._* 197 | 198 | # Files that might appear in the root of a volume 199 | .DocumentRevisions-V100 200 | .fseventsd 201 | .Spotlight-V100 202 | .TemporaryItems 203 | .Trashes 204 | .VolumeIcon.icns 205 | .com.apple.timemachine.donotpresent 206 | 207 | # Directories potentially created on remote AFP share 208 | .AppleDB 209 | .AppleDesktop 210 | Network Trash Folder 211 | Temporary Items 212 | .apdisk 213 | package-lock.json 214 | 215 | **/.vs/* 216 | **/bin/* 217 | **/obj/* 218 | **/nppBackup/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aact logo 2 | 3 | # Architecture As Code Tools 4 | [![test workflow](https://github.com/razonrus/ArchAsCode_Tests/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/razonrus/ArchAsCode_Tests/actions/workflows/test.yaml) 5 | 6 | Инструменты для работы с архитектурой в формате "as Code": 7 | 1. Код и примеры покрытия тестами микросервисной архитектуры, описанной в plantuml ([#](#%D0%BF%D0%BE%D0%BA%D1%80%D1%8B%D1%82%D0%B8%D0%B5-%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D1%8B-%D1%82%D0%B5%D1%81%D1%82%D0%B0%D0%BC%D0%B8)) 8 | 2. Автогенерация архитектуры ([#](#%D0%B0%D0%B2%D1%82%D0%BE%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F-%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D1%8B-1)) 9 | 3. Тестирование архитектуры модульного монолита ([#](#%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BC%D0%BE%D0%B4%D1%83%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE-%D0%BC%D0%BE%D0%BD%D0%BE%D0%BB%D0%B8%D1%82%D0%B0)) 10 | 11 | [Планы развития инструментов и репозитория](https://github.com/Byndyusoft/aact/blob/main/roadmap.md). PullRequest'ы и Issues'ы приветствуются. 12 | 13 | [Справочник](https://github.com/Byndyusoft/aact/blob/main/patterns.md) принципов и паттернов проектирования с примерами покрытия их тестами (пополняется...) 14 | 15 | Телеграм-канал: [Архитектура распределённых систем](https://t.me/rsa_enc) 16 | 17 | ## Публичные материалы 18 | ### Раз архитектура — «as Code», почему бы её не покрыть тестами?! 19 | 20 | 21 | https://www.youtube.com/watch?v=POIbWZh68Cg $~~~~~~~~~~$ https://www.youtube.com/watch?v=tZ-FQeObSjY 22 | 23 | [Статья на Хабре](https://habr.com/ru/articles/800205/) 24 | 25 | ### Автогенерация архитектуры 26 |
27 | https://www.youtube.com/watch?v=fb2UjqjHGUE 28 | 29 | # Покрытие архитектуры тестами 30 | ## Что это, какую боль решает, и с чего начать? 31 | Раз архитектура — «as Code», почему бы её не покрыть тестами?! 32 | 33 | Тема идеи и данный открытый репозиторий вызвал неожиданную волну позитивных отзывов о попадании в яблочко болей и о применимости и полезности решения :) 34 | 35 | Подход помогает решить **проблемы неактуальности, декларативности и отсутствия контроля ИТ-архитектур и инфраструктуры** (ограничение и требование — архитектура и инфраструктура должны быть "as code"). 36 | 37 | Тесты проверяют 2 больших блока: 38 | - актуальность архитектуры реальному работающему в продакшне решению 39 | - соответствие "нарисованной" архитектуры выбранным принципам и паттернам проектирования 40 | 41 | Подробнее о подходе, решаемых проблемах, схеме работы представленного в репозитории примера и проверяемых в тестах репозитория принципах — на [слайдах](https://docs.google.com/presentation/d/16_3h1BTIRyREXO_oSqnjEbRJAnN3Z4aX/edit?usp=sharing&ouid=106100367728328513490&rtpof=true&sd=true). 42 | 43 | ### Схема работы 44 | 45 | 46 | ### Визуализация примера автоматически проверяемого принципа (отсутствие бизнес-логики в CRUD-сервисах) 47 | 48 | 49 | 50 | ## Пример архитектуры, которую покроем тестами 51 | [![C4](resources/architecture/Demo%20Tests.svg)](resources/architecture/Demo%20Tests.svg) 52 | 53 | ## Пример тестов 54 | 1. [Finds diff in configs and uml containers](https://github.com/razonrus/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L43C10-L43C48) — проверяет актуальность списка микросервисов на архитектуре и в [конфигурации инфраструктуры](https://github.com/razonrus/aact/tree/main/resources/kubernetes/microservices) 55 | 2. [Finds diff in configs and uml dependencies](https://github.com/razonrus/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L52C9-L52C9) — проверяет актуальность зависимостей (связей) микросервисов на архитектуре и в [конфигурации инфраструктуры](https://github.com/razonrus/aact/tree/main/resources/kubernetes/microservices) 56 | 3. [Check that urls and topics from relations exists in config](https://github.com/razonrus/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L86C5-L86C5) — проверяет соответствие между параметрами связей микросервисов (REST-урлы, топики kafka) на архитектуре и в [конфигурации инфраструктуры](https://github.com/razonrus/aact/tree/main/resources/kubernetes/microservices) 57 | 4. [Only acl can depence from external systems](https://github.com/razonrus/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L111C7-L111C49) — проверяет, что не нарушен выбранный принцип построения интеграций с внешними системами только через ACL (Anti Corruption Layer). Проверяет, что только acl-микросервисы имеют зависимости от внешних систем. 58 | 5. [Connect to external systems only by API Gateway or kafka](https://github.com/razonrus/aact/blob/721edde3767dc0e51d19c80c3b6adba9fbf7b007/test/architecture.test.ts#L127C16-L127C16) — проверяет, что все внешние интеграции идут через API Gateway или через kafka 59 | 60 | # Автогенерация архитектуры 61 | ## Генерация архитектуры из описанной «as Code» инфраструктуры 62 | Добавил [код](https://github.com/Byndyusoft/aact/blob/39d8141a241f1139d5e58061f8674a22341b72de/test/architecture.test.ts#L214), который полностью с нуля генерирует архитектуру в plantuml по данным из IaC. 63 | 64 | Сравнение ~~белковой~~ составленной вручную архитектуры и сгенерированной. 65 | ### Ручная: 66 | [![C4](resources/architecture/Demo%20Tests.svg)](resources/architecture/Demo%20Tests.svg) 67 | ### Сгенерированная: 68 | [![C4](resources/architecture/Demo%20Generated.svg)](resources/architecture/Demo%20Generated.svg) 69 | 70 | # Тестирование модульного монолита 71 | 72 | Тестами можно покрывать не только архитектуру микросервисов, но архитектуру монолитов, особенно, если они модульные. 73 | 74 | * [Тест архитектуры модульного монолита на C#](https://github.com/Byndyusoft/aact/tree/main/ModularMonolith) 75 | 76 | # Тестирование на основе информации из кода 77 | 78 | Информацию об архитектуре реализованной системы можно извлечь и из ее кода, особенно, если он написан качественно;) 79 | 80 | * [Извлечение информации об архитектуры системы из ее кода](https://github.com/Byndyusoft/byndyusoft-architecture-testing) 81 | -------------------------------------------------------------------------------- /test/architecture.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | 5 | import { Stdlib_C4_Dynamic_Rel } from "plantuml-parser"; 6 | 7 | import { Container } from "../src/entities"; 8 | import { 9 | DeployConfig, 10 | loadMicroserviceDeployConfigs, 11 | mapFromConfigs, 12 | } from "../src/deployConfigs"; 13 | import { 14 | loadPlantumlElements, 15 | mapContainersFromPlantumlElements, 16 | } from "../src/plantuml"; 17 | 18 | const SystemExternalType = "System_Ext"; 19 | const ContainerType = "Container"; 20 | const AsyncTag = "async"; 21 | const RestTag = "REST"; 22 | 23 | describe("Architecture", () => { 24 | let deployConfigs: DeployConfig[]; 25 | let containersFromPuml: Container[]; 26 | let deployConfigsForContainers: DeployConfig[]; 27 | 28 | beforeAll(async () => { 29 | deployConfigs = mapFromConfigs(await loadMicroserviceDeployConfigs()); 30 | 31 | const pumlElements = await loadPlantumlElements("C4L2.puml"); 32 | containersFromPuml = 33 | mapContainersFromPlantumlElements(pumlElements).allContainers; 34 | 35 | deployConfigsForContainers = deployConfigs.filter((x) => 36 | containersFromPuml.find((y) => x.name === y.name), 37 | ); 38 | }); 39 | 40 | beforeEach(() => { 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, n/global-require 42 | global.console = require("console"); 43 | }); 44 | 45 | afterEach(() => { 46 | global.console = console; 47 | }); 48 | 49 | it("find diff in configs and uml containers", () => { 50 | const namesFromDeploy = deployConfigs.map((x) => x.name); 51 | const containerNamesFromPuml = containersFromPuml 52 | .filter((x) => x.type === ContainerType) 53 | .map((x) => x.name); 54 | 55 | expect(namesFromDeploy).toStrictEqual(containerNamesFromPuml); 56 | }); 57 | 58 | it("find diff in configs and uml dependencies", () => { 59 | let firstFailedConfig: DeployConfig = { 60 | name: "", 61 | sections: [], 62 | }; 63 | for (const config of deployConfigsForContainers) { 64 | const containerFromPuml = getPumlContainer(config.name); 65 | if (!containerFromPuml) continue; 66 | 67 | let log = `Container name ${config.name} `; 68 | if (checkSections(config, containerFromPuml, containersFromPuml)) 69 | log = `${log}✅`; 70 | else { 71 | log = `${log}❌`; 72 | if (!firstFailedConfig.name) firstFailedConfig = config; 73 | } 74 | 75 | console.log(log); 76 | } 77 | 78 | const pumlContainer = getPumlContainer(firstFailedConfig.name); 79 | if (!pumlContainer) return; 80 | console.log(`--------------------------------------------------------`); 81 | console.log(`First failed container name ${firstFailedConfig.name}`); 82 | console.log(`--------------------------------------------------------`); 83 | expect( 84 | checkSections(firstFailedConfig, pumlContainer, containersFromPuml, true), 85 | ).toBeTruthy(); 86 | 87 | function getPumlContainer(name: string): Container | undefined { 88 | return containersFromPuml.find((x) => x.name === name); 89 | } 90 | }); 91 | 92 | it("check that urls and topics from relations exist in config", () => { 93 | let pass = true; 94 | for (const container of containersFromPuml) { 95 | const config = deployConfigsForContainers.find( 96 | (x) => x.name === container.name, 97 | ); 98 | if (!config) continue; 99 | let log = `Container name ${container.name} `; 100 | for (const relation of container.relations) { 101 | const items = relation.technology?.split(", "); 102 | if ( 103 | items && 104 | !items.every((i: string) => 105 | config.sections.some((s) => s.prod_value === i), 106 | ) 107 | ) { 108 | log = `${log}❌ ${relation.to.name} ${items}`; 109 | pass = false; 110 | } 111 | } 112 | console.log(log); 113 | } 114 | expect(pass).toBeTruthy(); 115 | }); 116 | 117 | it("only acl can depend on external systems", () => { 118 | let pass = 0; 119 | for (const container of containersFromPuml) { 120 | let log = `Container name ${container.name} `; 121 | const externalRelations = container.relations.filter( 122 | (r) => r.to.type === SystemExternalType, 123 | ); 124 | if (!container.tags?.includes("acl") && externalRelations.length > 0) { 125 | log = `${log}❌ ${externalRelations.map((x) => x.to.name).toString()}`; 126 | pass = pass + externalRelations.length; 127 | } else log = `${log}✅`; 128 | console.log(log); 129 | } 130 | expect(pass).toBe(0); 131 | }); 132 | 133 | it("connect to external systems only by API Gateway or kafka", () => { 134 | let pass = true; 135 | for (const container of containersFromPuml) { 136 | let log = `Container name ${container.name} `; 137 | for (const r of container.relations.filter( 138 | (relation) => relation.to.type === SystemExternalType, 139 | )) { 140 | if ( 141 | !r.technology 142 | ?.split(", ") 143 | .every( 144 | (i: string) => 145 | i.startsWith("https://gateway.int.com:443/") || /-v\d$/.exec(i), 146 | ) 147 | ) { 148 | log = `${log}❌ ${r.to.name}`; 149 | pass = false; 150 | } 151 | } 152 | console.log(log); 153 | } 154 | expect(pass).toBeTruthy(); 155 | }); 156 | 157 | function checkSections( 158 | config: DeployConfig, 159 | containerFromPuml: Container, 160 | allContainersFromPuml: Container[], 161 | verbose = false, 162 | ): boolean { 163 | return ( 164 | config.sections.every((section) => { 165 | const result = 166 | containerFromPuml.relations.some((r) => { 167 | let result = false; 168 | if (r.tags?.includes(AsyncTag)) 169 | result = r.technology?.includes(section.prod_value) === true; 170 | if (!result && (!r.tags || r.tags.includes(RestTag))) 171 | result = 172 | r.to.name === section.name && 173 | (r.technology?.includes(section.prod_value) || 174 | r.to.type !== SystemExternalType); 175 | return result; 176 | }) || 177 | allContainersFromPuml.some((pumlc) => 178 | pumlc.relations.some( 179 | (r) => 180 | r.to.name === config.name && 181 | section.prod_value && 182 | r.technology?.includes(section.prod_value), 183 | ), 184 | ); 185 | if (!result && verbose) 186 | console.log( 187 | `In Config But Not In PUML: ${section.name} ${section.prod_value}`, 188 | ); 189 | return result; 190 | }) && 191 | [ 192 | ...containerFromPuml.relations, 193 | ...allContainersFromPuml.flatMap((x) => 194 | x.relations.filter( 195 | (r) => r.to.name === config.name && r.tags?.includes(AsyncTag), 196 | ), 197 | ), 198 | ].every((relation) => { 199 | const result = 200 | relation.to.name.endsWith("_db") || 201 | config.sections.some( 202 | (configSection) => 203 | configSection.name === relation.to.name || 204 | (configSection.prod_value && 205 | relation.technology?.includes(configSection.prod_value)), 206 | ); 207 | 208 | if (!result && verbose) 209 | console.log( 210 | `In PUML But Not In Config: ${relation.technology} ${relation.to.name}`, 211 | ); 212 | return result; 213 | }) 214 | ); 215 | } 216 | 217 | it("generate puml from configs", async () => { 218 | const filepath = path.join( 219 | process.cwd(), 220 | "resources/architecture", 221 | "generated.puml", 222 | ); 223 | let data = `@startuml "Demo Generated" 224 | !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml 225 | LAYOUT_WITH_LEGEND() 226 | AddRelTag("async", $lineStyle = DottedLine()) 227 | AddElementTag("acl", $bgColor = "#6F9355") 228 | Boundary(project, "Our system"){ 229 | `; 230 | 231 | const rels: Stdlib_C4_Dynamic_Rel[] = []; 232 | const extSystems: string[] = []; 233 | const intContainers: string[] = []; 234 | 235 | for (const config of deployConfigs) { 236 | data += `Container(${config.name}, "${config.name.replaceAll("_", " ")}"`; 237 | if (config.name.endsWith("acl")) data += `, "", "", $tags="acl"`; 238 | data += `) 239 | `; 240 | intContainers.push(config.name); 241 | 242 | if (config.environment?.PG_CONNECTION_STRING) { 243 | const dbName = config.name + "_db"; 244 | data += `ContainerDb(${dbName}, "DB") 245 | `; 246 | intContainers.push(dbName); 247 | addRel(config.name, dbName, "", false); 248 | } 249 | } 250 | data += `} 251 | `; 252 | 253 | for (const config of deployConfigs) { 254 | for (const section of config.sections) { 255 | if (section.name.startsWith("kafka")) { 256 | const containers = deployConfigs.filter( 257 | (x) => 258 | x.name !== config.name && 259 | x.sections.some((s) => s.prod_value === section.prod_value), 260 | ); 261 | for (const rel of containers) { 262 | addRel(config.name, rel.name, "", true); 263 | } 264 | if (containers.length == 0) { 265 | addRel( 266 | config.name, 267 | section.name.replaceAll("kafka_", "").replaceAll("_topic", ""), 268 | section.prod_value, 269 | true, 270 | ); 271 | } 272 | } else { 273 | addRel(config.name, section.name, section.prod_value, false); 274 | } 275 | } 276 | } 277 | data += "@enduml"; 278 | await fs.writeFile(filepath, data); 279 | 280 | function addRel( 281 | fromName: string, 282 | toName: string, 283 | transport: string, 284 | async: boolean, 285 | ): void { 286 | if ( 287 | !rels.some( 288 | (x) => 289 | (x.from === fromName && x.to === toName) || 290 | (x.to === fromName && x.from === toName), 291 | ) 292 | ) { 293 | var transportAttribute = ""; 294 | if (!intContainers.includes(toName) && !extSystems.includes(toName)) { 295 | data += `System_Ext(${toName}, "${toName}", " ") 296 | `; 297 | extSystems.push(toName); 298 | transportAttribute = `, "${transport}"`; 299 | } 300 | 301 | data += `Rel(${fromName}, ${toName}, ""${transportAttribute}`; 302 | if (async) data += `, $tags="async"`; 303 | data += `) 304 | `; 305 | 306 | rels.push({ 307 | from: fromName, 308 | to: toName, 309 | } as Stdlib_C4_Dynamic_Rel); 310 | } 311 | } 312 | }); 313 | }); 314 | -------------------------------------------------------------------------------- /resources/architecture/Demo Coupling And Cohesion.svg: -------------------------------------------------------------------------------- 1 | Наш проектКонтекст 1Контекст 2Микросервис AМикросервис BМикросервис CМикросервис XМикросервис YВнешняя система.......Legend personsystemcontainerexternal personexternal systemexternal container -------------------------------------------------------------------------------- /ModularMonolith/ModularMonolith.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34309.116 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monolith.Api", "Monolith.Api\Monolith.Api.csproj", "{B642214F-7488-4103-956C-3197E93EBB0D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monolith.ArchitectureTests", "Monolith.ArchitectureTests\Monolith.ArchitectureTests.csproj", "{1E57D65B-7475-41C6-9716-B031E1963824}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schedule", "Schedule", "{A53C2874-5D06-4A63-AEA6-4D14AA9215BF}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shedule.Domain", "Schedule\Shedule.Domain\Shedule.Domain.csproj", "{9B6F7EC0-902C-4CA8-8BC0-CE77AC573587}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shedule.DataAccess", "Schedule\Shedule.DataAccess\Shedule.DataAccess.csproj", "{88BCF242-4659-4E83-942B-3B34E3E64380}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shedule.Contract", "Schedule\Shedule.Contract\Shedule.Contract.csproj", "{3AF35E68-8E15-4EAC-9F12-3778C574E2EF}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProjectQuality", "ProjectQuality", "{EF83846F-3E6E-4FAF-989D-D981BB72893F}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.ProjectQuality.Domain", "ProjectQuality\Byndyusoft.ProjectQuality.Domain\Byndyusoft.ProjectQuality.Domain.csproj", "{F02E8B4B-5DB2-4D09-A575-84A8FB91ECBB}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.ProjectQuality.DataAccess", "ProjectQuality\Byndyusoft.ProjectQuality.DataAccess\Byndyusoft.ProjectQuality.DataAccess.csproj", "{94BE1387-1863-495E-BEA3-22787AA15E8C}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.ProjectQuality.Contracts", "ProjectQuality\Byndyusoft.ProjectQuality.Contracts\Byndyusoft.ProjectQuality.Contracts.csproj", "{C763CD40-576B-43D0-8D5B-11EDB20832B8}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Customers", "Customers", "{18838AEA-A9C8-435C-A280-9FB72240C1E5}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.Customers.Domain", "Customers\Byndyusoft.Customers.Domain\Byndyusoft.Customers.Domain.csproj", "{44B0309D-557A-4219-9FC5-B22F752C99E5}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.Customers.DataAccess", "Customers\Byndyusoft.Customers.DataAccess\Byndyusoft.Customers.DataAccess.csproj", "{C4F61F58-220D-4240-A485-F4EC84773EFD}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.Customers.Processing", "Customers\Byndyusoft.Customers.Processing\Byndyusoft.Customers.Processing.csproj", "{26A6E650-7EDA-4CC0-9347-AD07A7772C98}" 33 | EndProject 34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byndyusoft.Customers.Contracts", "Customers\Byndyusoft.Customers.Contracts\Byndyusoft.Customers.Contracts.csproj", "{6F025212-A7FA-40D7-8579-B3480011B325}" 35 | EndProject 36 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{8303A0D7-7840-44E3-BBD9-3FD41DE91664}" 37 | EndProject 38 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Domain", "Common\Common.Domain\Common.Domain.csproj", "{F3905B09-0691-49DD-A141-1C703685F3C0}" 39 | EndProject 40 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Contracts", "Common\Common.Contracts\Common.Contracts.csproj", "{A0BACE4B-7363-484E-B975-99061F99CDC8}" 41 | EndProject 42 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Emails", "Emails", "{5DB7C29E-170D-4177-86C4-CA49D38DE9A1}" 43 | EndProject 44 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emails.Domain", "Emails\Emails.Domain\Emails.Domain.csproj", "{5CD91BD6-AAB5-4995-BE9D-BEFDEE71562B}" 45 | EndProject 46 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emails.Contracts", "Emails\Emails.Contracts\Emails.Contracts.csproj", "{1EE2EF81-F845-48F9-9939-CE8E11E97FAC}" 47 | EndProject 48 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Audith", "Audith", "{F1CF7648-7720-43A6-9C5B-032131194303}" 49 | EndProject 50 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audit.Domain", "Audit\Audit.Domain\Audit.Domain.csproj", "{1CAD5D9E-FC8B-4FB7-8BB1-6708CAAB5CAC}" 51 | EndProject 52 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audit.Contracts", "Audit\Audit.Contracts\Audit.Contracts.csproj", "{A3D50678-6AEB-4A55-BF9B-42034EFA85E3}" 53 | EndProject 54 | Global 55 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 56 | Debug|Any CPU = Debug|Any CPU 57 | Release|Any CPU = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 60 | {B642214F-7488-4103-956C-3197E93EBB0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {B642214F-7488-4103-956C-3197E93EBB0D}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {B642214F-7488-4103-956C-3197E93EBB0D}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {B642214F-7488-4103-956C-3197E93EBB0D}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {1E57D65B-7475-41C6-9716-B031E1963824}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {1E57D65B-7475-41C6-9716-B031E1963824}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {1E57D65B-7475-41C6-9716-B031E1963824}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {1E57D65B-7475-41C6-9716-B031E1963824}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {9B6F7EC0-902C-4CA8-8BC0-CE77AC573587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {9B6F7EC0-902C-4CA8-8BC0-CE77AC573587}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {9B6F7EC0-902C-4CA8-8BC0-CE77AC573587}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {9B6F7EC0-902C-4CA8-8BC0-CE77AC573587}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {88BCF242-4659-4E83-942B-3B34E3E64380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {88BCF242-4659-4E83-942B-3B34E3E64380}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {88BCF242-4659-4E83-942B-3B34E3E64380}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {88BCF242-4659-4E83-942B-3B34E3E64380}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {3AF35E68-8E15-4EAC-9F12-3778C574E2EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {3AF35E68-8E15-4EAC-9F12-3778C574E2EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {3AF35E68-8E15-4EAC-9F12-3778C574E2EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {3AF35E68-8E15-4EAC-9F12-3778C574E2EF}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {F02E8B4B-5DB2-4D09-A575-84A8FB91ECBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 81 | {F02E8B4B-5DB2-4D09-A575-84A8FB91ECBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 82 | {F02E8B4B-5DB2-4D09-A575-84A8FB91ECBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {F02E8B4B-5DB2-4D09-A575-84A8FB91ECBB}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {94BE1387-1863-495E-BEA3-22787AA15E8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 85 | {94BE1387-1863-495E-BEA3-22787AA15E8C}.Debug|Any CPU.Build.0 = Debug|Any CPU 86 | {94BE1387-1863-495E-BEA3-22787AA15E8C}.Release|Any CPU.ActiveCfg = Release|Any CPU 87 | {94BE1387-1863-495E-BEA3-22787AA15E8C}.Release|Any CPU.Build.0 = Release|Any CPU 88 | {C763CD40-576B-43D0-8D5B-11EDB20832B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 89 | {C763CD40-576B-43D0-8D5B-11EDB20832B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 90 | {C763CD40-576B-43D0-8D5B-11EDB20832B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 91 | {C763CD40-576B-43D0-8D5B-11EDB20832B8}.Release|Any CPU.Build.0 = Release|Any CPU 92 | {44B0309D-557A-4219-9FC5-B22F752C99E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 93 | {44B0309D-557A-4219-9FC5-B22F752C99E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 94 | {44B0309D-557A-4219-9FC5-B22F752C99E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {44B0309D-557A-4219-9FC5-B22F752C99E5}.Release|Any CPU.Build.0 = Release|Any CPU 96 | {C4F61F58-220D-4240-A485-F4EC84773EFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 97 | {C4F61F58-220D-4240-A485-F4EC84773EFD}.Debug|Any CPU.Build.0 = Debug|Any CPU 98 | {C4F61F58-220D-4240-A485-F4EC84773EFD}.Release|Any CPU.ActiveCfg = Release|Any CPU 99 | {C4F61F58-220D-4240-A485-F4EC84773EFD}.Release|Any CPU.Build.0 = Release|Any CPU 100 | {26A6E650-7EDA-4CC0-9347-AD07A7772C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 101 | {26A6E650-7EDA-4CC0-9347-AD07A7772C98}.Debug|Any CPU.Build.0 = Debug|Any CPU 102 | {26A6E650-7EDA-4CC0-9347-AD07A7772C98}.Release|Any CPU.ActiveCfg = Release|Any CPU 103 | {26A6E650-7EDA-4CC0-9347-AD07A7772C98}.Release|Any CPU.Build.0 = Release|Any CPU 104 | {6F025212-A7FA-40D7-8579-B3480011B325}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 105 | {6F025212-A7FA-40D7-8579-B3480011B325}.Debug|Any CPU.Build.0 = Debug|Any CPU 106 | {6F025212-A7FA-40D7-8579-B3480011B325}.Release|Any CPU.ActiveCfg = Release|Any CPU 107 | {6F025212-A7FA-40D7-8579-B3480011B325}.Release|Any CPU.Build.0 = Release|Any CPU 108 | {F3905B09-0691-49DD-A141-1C703685F3C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 109 | {F3905B09-0691-49DD-A141-1C703685F3C0}.Debug|Any CPU.Build.0 = Debug|Any CPU 110 | {F3905B09-0691-49DD-A141-1C703685F3C0}.Release|Any CPU.ActiveCfg = Release|Any CPU 111 | {F3905B09-0691-49DD-A141-1C703685F3C0}.Release|Any CPU.Build.0 = Release|Any CPU 112 | {A0BACE4B-7363-484E-B975-99061F99CDC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 113 | {A0BACE4B-7363-484E-B975-99061F99CDC8}.Debug|Any CPU.Build.0 = Debug|Any CPU 114 | {A0BACE4B-7363-484E-B975-99061F99CDC8}.Release|Any CPU.ActiveCfg = Release|Any CPU 115 | {A0BACE4B-7363-484E-B975-99061F99CDC8}.Release|Any CPU.Build.0 = Release|Any CPU 116 | {5CD91BD6-AAB5-4995-BE9D-BEFDEE71562B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 117 | {5CD91BD6-AAB5-4995-BE9D-BEFDEE71562B}.Debug|Any CPU.Build.0 = Debug|Any CPU 118 | {5CD91BD6-AAB5-4995-BE9D-BEFDEE71562B}.Release|Any CPU.ActiveCfg = Release|Any CPU 119 | {5CD91BD6-AAB5-4995-BE9D-BEFDEE71562B}.Release|Any CPU.Build.0 = Release|Any CPU 120 | {1EE2EF81-F845-48F9-9939-CE8E11E97FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 121 | {1EE2EF81-F845-48F9-9939-CE8E11E97FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU 122 | {1EE2EF81-F845-48F9-9939-CE8E11E97FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU 123 | {1EE2EF81-F845-48F9-9939-CE8E11E97FAC}.Release|Any CPU.Build.0 = Release|Any CPU 124 | {1CAD5D9E-FC8B-4FB7-8BB1-6708CAAB5CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 125 | {1CAD5D9E-FC8B-4FB7-8BB1-6708CAAB5CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU 126 | {1CAD5D9E-FC8B-4FB7-8BB1-6708CAAB5CAC}.Release|Any CPU.ActiveCfg = Release|Any CPU 127 | {1CAD5D9E-FC8B-4FB7-8BB1-6708CAAB5CAC}.Release|Any CPU.Build.0 = Release|Any CPU 128 | {A3D50678-6AEB-4A55-BF9B-42034EFA85E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 129 | {A3D50678-6AEB-4A55-BF9B-42034EFA85E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 130 | {A3D50678-6AEB-4A55-BF9B-42034EFA85E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 131 | {A3D50678-6AEB-4A55-BF9B-42034EFA85E3}.Release|Any CPU.Build.0 = Release|Any CPU 132 | EndGlobalSection 133 | GlobalSection(SolutionProperties) = preSolution 134 | HideSolutionNode = FALSE 135 | EndGlobalSection 136 | GlobalSection(NestedProjects) = preSolution 137 | {9B6F7EC0-902C-4CA8-8BC0-CE77AC573587} = {A53C2874-5D06-4A63-AEA6-4D14AA9215BF} 138 | {88BCF242-4659-4E83-942B-3B34E3E64380} = {A53C2874-5D06-4A63-AEA6-4D14AA9215BF} 139 | {3AF35E68-8E15-4EAC-9F12-3778C574E2EF} = {A53C2874-5D06-4A63-AEA6-4D14AA9215BF} 140 | {F02E8B4B-5DB2-4D09-A575-84A8FB91ECBB} = {EF83846F-3E6E-4FAF-989D-D981BB72893F} 141 | {94BE1387-1863-495E-BEA3-22787AA15E8C} = {EF83846F-3E6E-4FAF-989D-D981BB72893F} 142 | {C763CD40-576B-43D0-8D5B-11EDB20832B8} = {EF83846F-3E6E-4FAF-989D-D981BB72893F} 143 | {44B0309D-557A-4219-9FC5-B22F752C99E5} = {18838AEA-A9C8-435C-A280-9FB72240C1E5} 144 | {C4F61F58-220D-4240-A485-F4EC84773EFD} = {18838AEA-A9C8-435C-A280-9FB72240C1E5} 145 | {26A6E650-7EDA-4CC0-9347-AD07A7772C98} = {18838AEA-A9C8-435C-A280-9FB72240C1E5} 146 | {6F025212-A7FA-40D7-8579-B3480011B325} = {18838AEA-A9C8-435C-A280-9FB72240C1E5} 147 | {F3905B09-0691-49DD-A141-1C703685F3C0} = {8303A0D7-7840-44E3-BBD9-3FD41DE91664} 148 | {A0BACE4B-7363-484E-B975-99061F99CDC8} = {8303A0D7-7840-44E3-BBD9-3FD41DE91664} 149 | {5CD91BD6-AAB5-4995-BE9D-BEFDEE71562B} = {5DB7C29E-170D-4177-86C4-CA49D38DE9A1} 150 | {1EE2EF81-F845-48F9-9939-CE8E11E97FAC} = {5DB7C29E-170D-4177-86C4-CA49D38DE9A1} 151 | {1CAD5D9E-FC8B-4FB7-8BB1-6708CAAB5CAC} = {F1CF7648-7720-43A6-9C5B-032131194303} 152 | {A3D50678-6AEB-4A55-BF9B-42034EFA85E3} = {F1CF7648-7720-43A6-9C5B-032131194303} 153 | EndGlobalSection 154 | GlobalSection(ExtensibilityGlobals) = postSolution 155 | SolutionGuid = {6380F1B3-CACC-491E-BA93-2132DFE0CC31} 156 | EndGlobalSection 157 | EndGlobal 158 | -------------------------------------------------------------------------------- /ModularMonolith/Architecture.svg: -------------------------------------------------------------------------------- 1 |
Common
Emails
Audit
Shedule
ProjectQuality
Customers
BFF
-------------------------------------------------------------------------------- /resources/architecture/Demo Generated.svg: -------------------------------------------------------------------------------- 1 | Our systembffcamundagoods aclinvoice aclinvoice repositoryDBstock acltask repositoryDBgoods_repository  goods2_repository  ext_system  invoice_input  invoice_output  stock  ...........[https:gateway.int.com:443/goods/v1]</size>//.[https:gateway.int.com:443/goods2/v1]</size>//.[https:gateway.int.com:443/ext/v1]</size>//.[orig-invoice-q8s-v1].[result-invoice-q8s-v1].[https:gateway.int.com:443/stock/v1]</size>//Legend personsystemcontainerexternal personexternal systemexternal container -------------------------------------------------------------------------------- /resources/architecture/Demo Tests.svg: -------------------------------------------------------------------------------- 1 | Our systemBFFGoods ACLCamundaTask RepositoryInvoice RepositoryInvoice ACLStock ACLDBDBStockExt SystemInvoice SourceInvoice ConsumerGoods RepositoryGoods2 RepositoryFront....[https:gateway.int.com:443/stock/v1]</size>//..[https:gateway.int.com:443/goods/v1]</size>//.[https:gateway.int.com:443/goods2/v1]</size>//........[orig-invoice-q8s-v1].[result-invoice-q8s-v1].[https:gateway.int.com:443/ext/v1]</size>//Legend personsystemcontainerexternal personexternal systemexternal container -------------------------------------------------------------------------------- /ModularMonolith/DetailedArchitecture.svg: -------------------------------------------------------------------------------- 1 |
BFF
Monolith.Api
Customers
Byndyusoft.Customers.Domain
Byndyusoft.Customers.Contracts
Byndyusoft.Customers.Processing
Byndyusoft.Customers.DataAccess
ProjectQuality
Byndyusoft.ProjectQuality.Domain
Byndyusoft.ProjectQuality.Contracts
Byndyusoft.ProjectQuality.DataAccess
Shedule
Shedule.Domain
Shedule.Contract
Shedule.DataAccess
Audit
Audit.Domain
Audit.Contracts
Emails
Emails.Domain
Emails.Contracts
Common
Common.Domain
Common.Contracts
--------------------------------------------------------------------------------