Skip to main content

Tactical Design & Clean Architecture

A rigorous implementation of Clean Architecture principles in Golang, decoupling core financial domain logic from volatile infrastructure and delivery frameworks.

TL;DR

Engineered a Clean Architecture across 6 Golang microservices. Achieved a highly testable domain layer by strictly enforcing Dependency Inversion, ensuring business rules remain completely isolated from gRPC protocols, NATS events, and PostgreSQL/GORM schemas.

Back End Flow

The Challenge & Impact

  • The Challenge: Financial systems require handling real-time data streams, complex business rules, and high-throughput API requests simultaneously. Tying these directly to database schemas or web frameworks creates a brittle "Big Ball of Mud," leading to race conditions, memory leaks, and untestable code.
  • The Objective: To enforce a strict architectural boundary—where dependencies only point inward toward the domain—while heavily optimizing the Delivery layer for concurrency and the Repository layer for query performance.
  • The Impact: Delivered a highly concurrent, memory-safe backend. Standardized the codebase by strictly separating DTOs, Domain Objects, and DB Schemas, which eliminated data leakage and enabled granular, highly parallel unit testing across all layers.

Architecture & Execution

Tech Stack

Go, Clean Architecture, FastHTTP, gRPC, WebSockets, GORM, PostgreSQL, Redis

1. The Layered Dependency Model

To implement this architecture, I meticulously designed dedicated data models for each layer to ensure a clear separation of concerns:

  1. Domain (Entities): Pure Go structs defining core financial business rules and invariants.
  2. Usecase (Execution & Inter-Service): The core orchestrator of the system. Its responsibilities are strictly scoped to:
    • Receiving DTOs from the Controller and mapping them to internal Domain Objects.
    • Coordinating the execution flow of complex business logic.
    • Managing domain state via abstracted Repository interfaces.
    • Handling cross-boundary, inter-service gRPC communications.
  3. Repository (DB Schema Objects): Infrastructure implementation. Executes queries and maps underlying DB schemas directly to Domain Objects.
  4. Controller (gRPC Entry Point): The primary gRPC entry point, callable by the API Gateway or other internal microservices. Its responsibilities are strictly scoped to:
    • Creating and validating Data Transfer Objects (DTOs).
    • Orchestrating the appropriate Usecases.
    • Dispatching system and domain events.
    • Translating internal Domain errors into standardized gRPC status codes for the client.

Clean Architecture

👉 Deep Dive on the LZStock Tech Blog: Back-end with DDD and Clean Architecture

2. Tactical Domain Modeling

To ensure the core business logic remains pristine and framework-agnostic, I strictly adhered to Domain-Driven Design (DDD) tactical patterns within the Domain Layer. By clearly defining the responsibility boundaries for Aggregates, Entities, and Value Objects, I prevented business logic from leaking into the UseCase or Infrastructure layers.

Aggregate (The Transactional Boundary)

Enforces overarching business rules, domain constraints, and cross-entity validation. It acts as the single entry point (Root) to guarantee data consistency.

Examples: Business limit enforcement (e.g., maximum daily trade volume), duplicate transaction checks, and domain-level permission logic.

Entity (Identity & Lifecycle)

Maintains unique identity over time. Handles entity completeness, required fields, and basic structural validation within its own scope.

Examples: Unique ID validation, checking required fields upon creation, and maintaining basic structural integrity.

Value Object (Immutable Traits)

Represents a descriptive aspect of the domain with no conceptual identity. Handles format, length, character rules, and primitive value validation.

Examples: Email format validation, price range constraints, and currency string length checks.

👉 Deep Dive on the LZStock Tech Blog: Domain Object Design

3. Algorithmic Highlight: Autocomplete Search (Ternary Search Tree)

Instead of relying on slow database LIKE queries for stock ticker auto-completion, I implemented a Ternary Search Tree (TST) in memory.

  • Execution: The tree building process is initialized asynchronously. I utilized an error channel (err chan) to gracefully collect and aggregate errors from concurrent goroutines during the build phase.
  • Result: This highly optimized TRIE data structure drastically reduced search latency, providing buttery-smooth UX for users searching through thousands of financial symbols.

👉 Deep Dive on the LZStock Tech Blog: Full implementation of Auto Complete Search Tree


Advanced Mechanics 1: Concurrency & Background Tasks

Thread-safe Session Management for Stock Price Streaming

⚙️ Check details
  • Dual-Level RWMutex Strategy: Implemented a thread-safe session manager to maximize throughput for concurrent price updates, preventing race conditions during high-frequency API calls.
  • Self-Healing Lifecycle Manager: Developed a background routine with TTL (Time-To-Live) logic to automate resource reclamation and prevent memory leaks.

👉 Deep Dive on the LZStock Tech Blog: Thread-safe Session Management

Goroutine Orchestration

⚙️ Check details

Utilized the errgroup package to concurrently execute and coordinate multiple goroutines, ensuring graceful shutdown and centralized error propagation across gRPC streaming and FastHTTP WebSocket tasks.

👉 Deep Dive on the LZStock Tech Blog: Websocket Integration

Go Generics Worker Pool

⚙️ Check details

Implemented a robust, concurrent Worker Pool leveraging Go's Genericity (Go 1.18+). This foundation allows for specialized, reusable type workers, ensuring high performance, maximum flexibility, and compile-time type safety for background tasks.

👉 Deep Dive on the LZStock Tech Blog: Background Tasks

Trigger Methods Invocation Rule

⚙️ Check details

Established clear rules of thumb for internal service invocation:

  • Controller: Single entry point for External/Traced Internal Events.
  • Usecase: Entry point for Untraced, Complex Logic (Event/Worker).
  • Repository: Entry point for Untraced, Simple CRUD (Event/Worker).

👉 Deep Dive on the LZStock Tech Blog: Trigger Methods

Advanced Mechanics 2: Error Handling & Testing Strategy

The Error Chain & Recovery

⚙️ Check details
  • Three-Layered Scope: Implemented granular error handling (App internal error, gRPC error code, HTTP error). Internal details are strictly masked from end-users to prevent information leakage.
  • Error Wrapping: Utilized fmt.Errorf with the %w verb for chaining, ensuring traceable logs.
  • Standardization: Centralized gRPC error handling via a HandleGRPCErr utility and mapped internal errors to user-friendly UI messages using a CreateDashboardErrMessageMap.
  • Panic Recovery: Developed a custom Recover utility to gracefully catch fatal panics and dump formatted stack traces for debugging without crashing the service.

👉 Deep Dive on the LZStock Tech Blog: Error Handling

Strategic Testing Focus

⚙️ Check details

By utilizing Dependency Inversion, testing is strategically distributed:

  • Domain: Unit Testing to verify core invariants in isolation.
  • Use-Case: Mocking external dependencies to ensure correct logic orchestration and error handling.
  • Repository: Integration Testing for data correctness and concurrency handling.
  • Controller: Verifying API contracts, request mapping, and the error propagation chain.
  • E2E Testing: Validating the complete request-response cycle (including Auth) from the client's perspective.

👉 Deep Dive on the LZStock Tech Blog: Testing Strategies

Advanced Mechanics 3: Database Design & Optimization

Pagination Strategy Evaluation

⚙️ Check details

Analyzed and implemented appropriate strategies based on use cases. Chose Cursor-based pagination for infinite scrolling performance, while evaluating Offset-based (for direct page access) and Database Cursor mechanisms.

👉 Deep Dive on the LZStock Tech Blog: Design Pagination Strategies

Preventing N+1 Queries

⚙️ Check details

Solved the N+1 problem by evaluating the trade-offs between GORM Eager Loading (Preloading) versus direct JOIN queries.

👉 Deep Dive on the LZStock Tech Blog: Prevent N+1 Problem

Execution Plans & Indexing

⚙️ Check details

Leveraged SQL Execution Plans for query tuning. Researched and mapped a scalability roadmap including B-tree, Hash, GIN (for Full-Text Search/JSONB), Covering Indexes (for index-only scans), and Partial Indexes to reduce index bloat.

👉 Deep Dive on the LZStock Tech Blog: SQL Execution Plans for Query Tuning

👉 Deep Dive on the LZStock Tech Blog: Indexing Exploration

Advanced Mechanics 4: API Gateway, Security

FastHTTP Integration

⚙️ Check details
  • Utilized high-performance libraries (FastHTTP and FastHTTP-routing) for efficient request handling.
  • Implemented robust versioning for the RESTful API endpoints.
  • Integrated gRPC backend services by wrapping connections within the RESTful API layer.

👉 Deep Dive on the LZStock Tech Blog: API Design and Versioning

Security Middleware

⚙️ Check details
  • Security Middleware: Implemented Route-based Access Control (RBAC) for authentication and a robust CORS strategy separating Development (loose) and Production (strict) environments.

👉 Deep Dive: See more details on the LZStock tech blog: RBAC 👉 Deep Dive: See more details on the LZStock tech blog: CORS Middleware

🌟 Present-Day Retrospective

Algorithmic & Architectural Reflections on State & Scaling:

  1. Algorithmic Debt: TST Balancing: While the in-memory Ternary Search Tree (TST) vastly outperformed database queries for autocomplete, the initial asynchronous build phase revealed a critical structural flaw. Because the stock tickers were ingested and inserted alphabetically (A to Z), the tree became highly unbalanced, essentially degrading its time complexity from $O(\log N)$ to $O(N)$ in worst-case branches. To resolve this, future iterations will pre-sort the dataset and insert elements recursively from the median, guaranteeing a perfectly balanced tree structure and predictable sub-millisecond latency.
  2. The Distributed State Bottleneck: While the dual-level RWMutex and self-healing lifecycle manager worked flawlessly for a single-node setup, I quickly identified horizontal scaling bottlenecks in local state management. This reflection is driving the proposed migration toward distributed event-streaming via NATS JetStream to completely decouple the streaming engine state.
  3. Worker Pool Evolution: The Go Generics Worker Pool provides excellent compile-time type safety and performance for local background tasks. However, for a fully resilient microservices architecture, I plan to transition these asynchronous jobs to a JetStream-based event-driven approach, ensuring persistence and guaranteed delivery across service restarts.