Skip to main content

7 posts tagged with "Architecture"

Layer boundaries, dependency direction and the design decisions you pay for as a system grows.

View All Tags

Thêm 4 index làm INSERT chậm 6 lần: cái giá không ai nhắc khi bảo bạn đánh index

· 13 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

Index tăng tốc đọc bằng cách trả giá ở mỗi lần ghi, và cái giá đó lớn hơn hầu hết người ta hình dung. Đo trên PostgreSQL 16 với cùng 200.000 dòng: bảng không index chèn xong trong 287,8 ms và chiếm 10,2 MB; bảng có bốn index mất 1.722,3 ms và chiếm 27 MB. Tức là chậm gấp 6 lần và tốn gấp 2,6 lần dung lượng — chỉ để thêm bốn cấu trúc mà có thể chẳng câu truy vấn nào dùng tới. Câu hỏi đúng không phải "cột này có nên có index không" mà là "phần đọc tiết kiệm được có bù nổi phần ghi phải trả không".

Mọi bài viết về tối ưu database đều kết thúc bằng lời khuyên thêm index. Rất ít bài nói về hoá đơn đi kèm, và càng ít bài đưa con số.

Bài này đào sâu bài Thiết kế database và bài Index trong series học SQL 30 ngày. Mọi số liệu đo thật trên PostgreSQL 16.11.

One Vietnamese Diacritic Killed an API Call: cf-ipcity, HttpClient and the ASCII Limit

· 10 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

Cloudflare injects cf-ipcity into incoming requests, and for visitors in Vietnam the value is Hồ Chí Minh — with diacritics, which means non-ASCII. Our .NET service forwarded every incoming header verbatim onto its outgoing calls, so that value landed in HttpClient. The surprise is that Headers.Add does not throw, and TryAddWithoutValidation returns true; everything only blows up at SendAsync with HttpRequestException: Request headers must contain only ASCII characters, and not a single byte leaves the process. The bug is neither Cloudflare's nor .NET's — it is in an application that forwards every header unconditionally.

The setting is an e-commerce loyalty platform serving roughly three million customers. I have removed the client's name and every identifying detail; what remains is the technical part.

Everything looked normal. The API was running. The Kubernetes pods were healthy. The database was fine. Requests were reaching the application. But one HTTP call to an internal service kept failing in production — and only in production.

What broke it, it turned out, was the name of the user's own city.

Forwarding Headers in ASP.NET Core: Why 'Forward Everything' Is an Architectural Bug

· 11 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

Copying every incoming header onto an outgoing request is an anti-pattern, and it fails in three different directions: correctness (one non-ASCII value makes HttpClient throw), security (you trust and relay data the client set itself), and architecture (headers injected by infrastructure become part of your application's contract). The fix is an allowlist — an explicit list of headers permitted to pass — placed in a shared DelegatingHandler. When you write that handler, mind one trap: it does not live in the request's scope.

This post closes a three-part series that began with a production incident on an e-commerce loyalty platform of roughly three million customers, where Cloudflare's cf-ipcity: Hồ Chí Minh header broke an internal call. The first part told the story, the second explained why headers cannot carry Vietnamese. This one answers what is left: how do you rewrite that code correctly?

Traefik + Cloudflare: Why Your Certificates All Expire on Day 60

· 17 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

When a domain is proxied through Cloudflare (the orange cloud) with SSL mode Full (strict), ACME HTTP-01 cannot work: Let's Encrypt requests http://domain/.well-known/acme-challenge/... on port 80, Cloudflare receives it and calls back to your origin over HTTPS:443 — where Traefik's challenge handler does not live. You need a certificate to get through Cloudflare, and you need to get through Cloudflare to obtain a certificate. You can work around the first issuance by temporarily switching the cloud to grey, but the automatic renewal around day 60 will fail silently and every site will expire at the same time. The way out is DNS-01: validation through a TXT record via the Cloudflare API, with no request ever touching the origin. The price is that certificates can only be issued for zones your API token can see.

This VPS runs 14 containers behind one shared reverse proxy: 10 WordPress sites, a .NET API, an Angular app, an event web app, and Traefik itself. Thirteen hostnames, all behind Cloudflare, all needing HTTPS, and nobody wanting to renew a certificate by hand.

The architecture is boring in the best way. What is interesting sits between Cloudflare and Let's Encrypt — two systems each doing exactly their job, which together produce a circular dependency you only discover on the day your certificates expire. That is, two months after everything appeared to be finished.

Unit of Work in .NET and ABP: SaveChangesAsync Is Not a Commit

· 10 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

EF Core's DbContext is already a Unit of Work: it collects changes in the change tracker and pushes them to the database inside one transaction when you call SaveChanges. Writing your own IUnitOfWork purely to call SaveChanges is therefore usually redundant. ABP is a different matter entirely: there, the Unit of Work is ambient, opens automatically per request, and SaveChangesAsync inside a UoW does not commit the transaction — only CompleteAsync does. Misreading that one point is the source of most "the data is there sometimes" bugs.

Unit of Work is one of the most frequently reimplemented patterns in the .NET world, and also the one most frequently reimplemented for no reason. This post splits into two parts: what you actually need with plain EF Core, and what ABP has already done for you that you should understand before touching it.