Evolution of Backend Architecture: From MVC to Microservices and Domain-Driven Design

This article recounts the author's three‑year journey building a Python‑based HR SaaS backend, detailing the architectural evolution from an initial Django MVC prototype through service splitting and microservices with Kong and custom RPC, to a domain‑driven design approach, and shares lessons learned and optimizations.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Evolution of Backend Architecture: From MVC to Microservices and Domain-Driven Design

Before joining Tencent, the author spent three years as a backend developer at a previous company, building a human‑resource SaaS product that served web, Android/iOS, and mini‑program clients via RESTful APIs written primarily in Python for rapid iteration.

The backend architecture evolved through four major stages: MVC, service splitting, microservices, and domain‑driven design.

1. MVC

In the early phase the team had fewer than five backend engineers. The focus was on quickly delivering a prototype, so Django was chosen for its speed. After designing the database schema, functional views were abstracted, and extensive placeholders were added to accommodate uncertain product requirements. Code standards and static analysis rules were established.

The overall architecture used Nginx for load balancing, multiple Django instances for request handling, Celery for asynchronous tasks, Redis for caching large data sets, and the Nginx Push Module for real‑time notifications.

Problems and optimizations:

Django's concurrency was limited; switched to uWSGI master/worker combined with gevent for coroutine‑based high concurrency.

Excessive Redis connections; employed redis‑py's built‑in connection pool for reuse.

Too many MySQL connections; introduced djorm‑ext‑pool for connection pooling.

Enabled gevent in Celery to allow concurrent task execution.

As the number of Django apps grew, deployment became cumbersome because every release required restarting all Django services, leading to downtime and overtime when issues arose.

2. Service Splitting

When the backend team expanded, responsibilities became more granular. Maintaining all code in a single repository became costly, so the existing modular Django apps facilitated a smoother split.

The initial split involved extracting shared code into a common Python library while keeping the same database and Redis instances; later, the database was scaled to multiple instances.

Inter‑service communication was performed via HTTP, with internal hosts configured in /etc/hosts. This avoided tight coupling but introduced configuration overhead.

Problems and optimizations:

The Nginx Push Module was outdated and limited in concurrent connections; it was replaced by a custom service built with Tornado and ZeroMQ (tormq) for message notifications.

HTTP calls lacked retry, error handling, and rate‑limiting, reducing overall reliability.

Frequent Nginx configuration changes caused deployment errors and made maintenance painful.

Authentication depended on a central user service; any change required redeploying multiple services.

3. Microservices Architecture

An OpenResty‑based Kong API Gateway was introduced at the edge, with custom plugins for authentication and rate limiting, offloading these cross‑cutting concerns from individual services.

To handle inter‑service RPC, a framework named doge was built on gevent + msgpack, using etcd for service discovery and providing built‑in load balancing, throttling, and high availability.

The team spent a month learning OpenResty and Go before implementing a short‑URL service in OpenResty. Kong was chosen because it is Lua‑based, easy to deploy, and its plugin system is straightforward; performance was secondary to developer productivity.

For RPC, the team evaluated the pure‑Python Thrift implementation ( thriftpy) but found the learning curve too steep for the small team, so they built doge, inspired by Weibo’s Motan.

4. Domain‑Driven Design

The next step was to extract a data‑service layer where each service encapsulates one or more bounded contexts, exposing only a single aggregate root via RPC. This decouples application services from data services, allowing higher‑level services to depend on lower‑level ones without tight coupling.

At the time of leaving the company, DDD was still in the learning phase and not yet deployed, but the author expects the backend to continue moving toward this direction.

# Summary

Technology selection should serve product and customer needs rather than follow trends blindly. Team size, skill distribution, and operational constraints often force compromises; finding optimal solutions within those constraints is the real challenge.

Service Mesh and other next‑generation microservice patterns are becoming mainstream, and although the author’s current work no longer involves microservices, they will continue to follow and study these developments.

END

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

architecturePythonDomain-Driven Design
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.