Database Centric vs Domain Centric Architecture
September 30, 2025 ยท View on GitHub
:warning: Warning
The code samples contain multiple ways and patterns to do things and not always be considered best practices or recommended for all situations.
Database Centric vs Domain Centric Architecture

Hexagonal Architecture

Onion Architecture

The Clean Architecture

Classic Three-layer Architecture

Modern Four-layer Architecture

Layer Dependencies

Layer Examples

Testing Pyramid

Vertical Slice Architecture (Modular Monolith)

Solution Structure



How to Run:
Update Configuration
Additional Configuration Sources
-
Open ClassifiedAds.WebMVC/appsettings.json and jump to ConfigurationSources section.
"ConfigurationSources": { "SqlServer": { "IsEnabled": false, "ConnectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#", "SqlQuery": "select [Key], [Value] from ConfigurationEntries" }, "AzureKeyVault": { "IsEnabled": false, "VaultName": "https://xxx.vault.azure.net/" } }, -
Get from Sql Server database:
"ConfigurationSources": { "SqlServer": { "IsEnabled": true, "ConnectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#", "SqlQuery": "select [Key], [Value] from ConfigurationEntries" }, }, -
Get from Azure Key Vault:
"ConfigurationSources": { "AzureKeyVault": { "IsEnabled": true, "VaultName": "https://xxx.vault.azure.net/" } }, -
Use Both:
"ConfigurationSources": { "SqlServer": { "IsEnabled": true, "ConnectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#", "SqlQuery": "select [Key], [Value] from ConfigurationEntries" }, "AzureKeyVault": { "IsEnabled": true, "VaultName": "https://xxx.vault.azure.net/" } },
Storage
-
Open ClassifiedAds.WebMVC/appsettings.json, ClassifiedAds.WebAPI/appsettings.json and jump to Storage section.
"Storage": { "Provider": "Local", }, -
Use Local Files:
"Storage": { "Provider": "Local", "Local": { "Path": "E:\\files" }, }, -
Use Azure Blob:
"Storage": { "Provider": "Azure", "Azure": { "ConnectionString": "xxx", "Container": "classifiedadds" }, }, -
Use Amazon S3:
"Storage": { "Provider": "Amazon", "Amazon": { "AccessKeyID": "xxx", "SecretAccessKey": "xxx", "BucketName": "classifiedadds", "RegionEndpoint": "ap-southeast-1" } },
Message Broker
-
Open below files and jump to Messaging section:
"Messaging": { "Provider": "RabbitMQ", } -
Use RabbitMQ
"Messaging": { "Provider": "RabbitMQ", "RabbitMQ": { "HostName": "localhost", "UserName": "guest", "Password": "guest", "ExchangeName": "amq.direct", "RoutingKeys": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" }, "QueueNames": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" } } } -
Use Kafka:
"Messaging": { "Provider": "Kafka", "Kafka": { "BootstrapServers": "localhost:9092", "Topics": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" }, } } -
Use Azure Queue Storage:
"Messaging": { "Provider": "AzureQueue", "AzureQueue": { "ConnectionString": "xxx", "QueueNames": { "FileUploadedEvent": "classifiedadds-fileuploaded", "FileDeletedEvent": "classifiedadds-filedeleted", "EmailMessageCreatedEvent": "classifiedadds-emailcreated", "SmsMessageCreatedEvent": "classifiedadds-smscreated" } } } -
Use Azure Service Bus:
"Messaging": { "Provider": "AzureServiceBus", "AzureServiceBus": { "ConnectionString": "xxx", "QueueNames": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" } } }
Logging
- Open and jump to Logging section of below files:
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.Background/appsettings.json
"Logging": { "LogLevel": { "Default": "Warning" }, "File": { "MinimumLogEventLevel": "Information" }, "Elasticsearch": { "IsEnabled": false, "Host": "http://localhost:9200", "IndexFormat": "classifiedads", "MinimumLogEventLevel": "Information" }, "EventLog": { "IsEnabled": false, "LogName": "Application", "SourceName": "ClassifiedAds.WebAPI" } }, - Write to Local file (./logs/log.txt). Always enabled.
"Logging": { "File": { "MinimumLogEventLevel": "Information" }, }, - Write to Elasticsearch:
"Logging": { "Elasticsearch": { "IsEnabled": true, "Host": "http://localhost:9200", "IndexFormat": "classifiedads", "MinimumLogEventLevel": "Information" }, }, - Write to Windows Event Log (Windows only):
"Logging": { "EventLog": { "IsEnabled": true, "LogName": "Application", "SourceName": "ClassifiedAds.WebAPI" } }, - Enable all options:
"Logging": { "LogLevel": { "Default": "Warning" }, "File": { "MinimumLogEventLevel": "Information" }, "Elasticsearch": { "IsEnabled": true, "Host": "http://localhost:9200", "IndexFormat": "classifiedads", "MinimumLogEventLevel": "Information" }, "EventLog": { "IsEnabled": true, "LogName": "Application", "SourceName": "ClassifiedAds.WebAPI" } },
Caching
- Open and jump to Caching section of below files:
"Caching": { "InMemory": { }, "Distributed": { } }, - Configure options for In Memory Cache:
"Caching": { "InMemory": { "SizeLimit": null }, }, - Use In Memory Distributed Cache (For Local Testing):
"Caching": { "Distributed": { "Provider": "InMemory", "InMemory": { "SizeLimit": null } } }, - Use Redis Distributed Cache:
"Caching": { "Distributed": { "Provider": "Redis", "Redis": { "Configuration": "xxx.redis.cache.windows.net:6380,password=xxx,ssl=True,abortConnect=False", "InstanceName": "" } } }, - Use Sql Server Distributed Cache:
dotnet tool install --global dotnet-sql-cache --version="5.0" dotnet sql-cache create "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#" dbo CacheEntries"Caching": { "Distributed": { "Provider": "SqlServer", "SqlServer": { "ConnectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#", "SchemaName": "dbo", "TableName": "CacheEntries" } } },
Monitoring
- Open and jump to Monitoring section of below files:
"Monitoring": { }, - Use Azure Application Insights:
"Monitoring": { "AzureApplicationInsights": { "IsEnabled": true, "InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "EnableSqlCommandTextInstrumentation": true } }, - Use OpenTelemetry:
"Monitoring": { "OpenTelemetry": { "IsEnabled": true, "ServiceName": "ClassifiedAds.WebAPI", "TraceEnabled": true, "MetricEnabled": true, "Otlp": { "IsEnabled": false, "Endpoint": "http://localhost:4317" } } }, - Use Both:
"Monitoring": { "AzureApplicationInsights": { "IsEnabled": true, "InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "EnableSqlCommandTextInstrumentation": true }, "OpenTelemetry": { "IsEnabled": true, "ServiceName": "ClassifiedAds.WebAPI", "TraceEnabled": true, "MetricEnabled": true, "Otlp": { "IsEnabled": false, "Endpoint": "http://localhost:4317" } } },
Interceptors
- Open and jump to Interceptors section of below files:
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.Background/appsettings.json
"Interceptors": { "LoggingInterceptor": true, "ErrorCatchingInterceptor": false },
Security Headers
- Open ClassifiedAds.WebAPI/appsettings.json and jump to SecurityHeaders section:
"SecurityHeaders": { "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0" }, - Open ClassifiedAds.WebMVC/appsettings.json and jump to SecurityHeaders section:
"SecurityHeaders": { "Content-Security-Policy": "form-action 'self'; frame-ancestors 'none'", "Feature-Policy": "camera 'none'", "Referrer-Policy": "strict-origin-when-cross-origin", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "X-XSS-Protection": "1; mode=block", "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0" },
Cross-Origin Resource Sharing (CORS)
- Open ClassifiedAds.WebAPI/appsettings.json and jump to CORS section:
"CORS": { "AllowAnyOrigin": false, "AllowedOrigins": [ "http://localhost:4200", "http://localhost:3000", "http://localhost:8080" ] },
External Login
- Open ClassifiedAds.IdentityServer/appsettings.json and jump to ExternalLogin section:
"ExternalLogin": { "AzureActiveDirectory": { "IsEnabled": true, "Authority": "https://login.microsoftonline.com/<Directory (tenant) ID>", "ClientId": "<Application (client) ID", "ClientSecret": "xxx" }, "Microsoft": { "IsEnabled": true, "ClientId": "<Application (client) ID", "ClientSecret": "xxx" }, "Google": { "IsEnabled": true, "ClientId": "xxx", "ClientSecret": "xxx" }, "Facebook": { "IsEnabled": true, "AppId": "xxx", "AppSecret": "xxx" } },
Sending Email
- Open ClassifiedAds.Background/appsettings.json and jump to Notification -> Email section:
"Notification": { "Email": { "Provider": "Fake", } } - Use SmtpClient:
"Notification": { "Email": { "Provider": "SmtpClient", "SmtpClient": { "Host": "localhost", "Port": "", "UserName": "", "Password": "", "EnableSsl": "" } } }
Sending SMS
- Open ClassifiedAds.Background/appsettings.json and jump to Notification -> Sms section:
"Notification": { "Sms": { "Provider": "Fake", } } - Use Twilio
"Notification": { "Sms": { "Provider": "Twilio", "Twilio": { "AccountSId": "", "AuthToken": "", "FromNumber": "" } } }
Run or Debug the Solution
-
Web MVC Home Page: https://localhost:44364/

-
Navigate to Health Checks UI https://localhost:44364/healthchecks-ui#/healthchecks and make sure everything is green.

-
Login on Identity Server:
- Option 1: Use default created account:
- User Name: phong@gmail.com
- Password: v*7Un8b4rcN@<-RN
- Option 2: Register new account at https://localhost:44367/Account/Register

- Option 1: Use default created account:
-
Open Blazor Home Page at: https://localhost:44331

How to Build and Run Single Page Applications:
-
Angular:
-
Navigate to folder: UIs/angular/
npm install ng serve -
Update environment.ts & environment.prod.ts
export const environment = { OpenIdConnect: { Authority: "https://localhost:44367", ClientId: "ClassifiedAds.Angular" }, ResourceServer: { Endpoint: "https://localhost:44312/api/" }, CurrentUrl: "http://localhost:4200/" }; -
Go to http://localhost:4200/

-
-
React:
-
Navigate to folder: UIs/reactjs/
npm install npm run dev -
Update environment.dev.tsx & environment.tsx
const environment = { OpenIdConnect: { Authority: "https://localhost:44367", ClientId: "ClassifiedAds.React" }, ResourceServer: { Endpoint: "https://localhost:44312/api/" }, CurrentUrl: "http://localhost:3000/" }; export default environment; -
Go to http://localhost:3000/

-
-
Vue:
- Navigate to folder: UIs/vuejs/
npm install npm run dev - Update environment.dev.ts & environment.dev.ts
const environment = { OpenIdConnect: { Authority: "https://localhost:44367", ClientId: "ClassifiedAds.Vue" }, ResourceServer: { Endpoint: "https://localhost:44312/api/" }, CurrentUrl: "http://localhost:8080/" }; export default environment;
- Navigate to folder: UIs/vuejs/
-
Go to http://localhost:8080/

-
Before Login, go to Identity Server https://localhost:44367/Client to make sure application clients have been registered:

How to Run on Docker Containers:
-
Add Migrations if you haven't done on previous steps:
- Install dotnet-ef cli:
dotnet tool install --global dotnet-ef --version="8.0" - Navigate to ClassifiedAds.Migrator and run these commands:
dotnet ef migrations add Init --context AdsDbContext -o Migrations/AdsDb
- Install dotnet-ef cli:
-
Navigate to Monolith and run:
docker compose build docker compose up -
Open Web MVC Home Page at: http://host.docker.internal:9003

-
Navigate to Health Checks UI http://host.docker.internal:9003/healthchecks-ui#/healthchecks and make sure everything is green.

-
Login on Identity Server:
- Use default created account: phong@gmail.com / v*7Un8b4rcN@<-RN
- Register new account at http://host.docker.internal:9000/Account/Register
-
Open Blazor Home Page at: http://host.docker.internal:9008

How to Run Integration & End to End Tests:
-
Update ClassifiedAds.IntegrationTests/appsettings.json
{ "OpenIdConnect": { "Authority": "https://localhost:44367", "ClientId": "ClassifiedAds.WebMVC", "ClientSecret": "secret", "RequireHttpsMetadata": "true" }, "WebAPI": { "Endpoint": "https://localhost:44312" }, "Login": { "UserName": "phong@gmail.com", "Password": "v*7Un8b4rcN@<-RN", "Scope": "ClassifiedAds.WebAPI" } } -
Download Chrome Driver

-
Update EndToEndUiTests/appsettings.json
{ "ChromeDriverPath": "D:\\Downloads\\chromedriver_win32\\72", "Login": { "Url": "https://localhost:44364/Home/Login", "UserName": "phong@gmail.com", "Password": "v*7Un8b4rcN@<-RN" } }
Application URLs:
https://github.com/phongnguyend/Practical.CleanArchitecture/wiki/Application-URLs
Roadmap:
https://github.com/phongnguyend/Practical.CleanArchitecture/wiki/Roadmap
Licence ๐
This repository is licensed under the MIT license.
Duende.IdentityServer License ๐
Duende.IdentityServer is available under both a FOSS (RPL) and a commercial license.
For the production environment, it is necessary to get a specific license, if you would like more information about the licensing of Duende.IdentityServer - please check this link.
The source code under /src/IdentityServers/Duende folder uses the source code from https://github.com/DuendeSoftware/IdentityServer.Quickstart.UI which is under the terms of the following license.