From MVC to Microservices: Lessons from Evolving a Python SaaS Backend

The author recounts a four‑stage evolution of a Python‑based SaaS backend—from a simple Django MVC prototype through service splitting, microservice adoption with Kong and custom RPC, to an exploratory domain‑driven design—highlighting practical problems, optimizations, and architectural trade‑offs.

ITPUB
ITPUB
ITPUB
From MVC to Microservices: Lessons from Evolving a Python SaaS Backend

1. MVC

In the early stage the team (max 5 developers) built a prototype quickly with Django, focusing on code conventions and view abstraction while leaving room for product changes. The architecture was simple: Nginx for load balancing, multiple Django instances, Celery for async tasks, Redis for caching, and Nginx Push Module for real‑time notifications.

Problems and optimizations:

Django’s concurrency limits were addressed by deploying uWSGI Master+Worker together with gevent for coroutine‑based high concurrency.

Excessive Redis connections were mitigated by using redis‑py’s built‑in connection pool for reuse.

MySQL connection overload was solved with the djorm‑ext‑pool connection‑pooling extension.

Celery was configured to use gevent, enabling concurrent task execution.

2. Service Splitting

As the backend team grew, responsibilities were divided into finer‑grained services. The existing Django project already organized code into high‑cohesion, low‑coupling apps, which eased the split. Initial separation involved extracting shared code into a common Python library while keeping the same database and Redis instances; later, multiple database instances were added to handle load.

Inter‑service communication was kept to a minimum; when needed, services called each other via HTTP, using internal host entries for address resolution.

Problems and optimizations:

The long‑unmaintained Nginx Push Module could not handle enough persistent connections, so a Tornado + ZeroMQ based service (tormq) was built to provide reliable notifications.

Additional pain points included manual host‑file management for service endpoints, lack of retry/error handling/traffic‑shaping in the call chain, and cumbersome Nginx configuration changes that often broke calls. Authentication relied on a central user‑center database, requiring coordinated redeployments when changed.

3. Microservice Architecture

The ingress layer adopted an OpenResty‑based Kong API Gateway, with custom plugins for authentication and rate limiting. Deployment scripts invoke Kong’s admin API to register new services and attach the required plugins.

To replace direct service‑to‑service calls, a gevent + msgpack RPC framework named doge was built, using etcd for service discovery and providing client‑side rate limiting, high availability, and load balancing.

Choosing an API gateway involved evaluating Golang and OpenResty implementations; Kong was selected for its Lua‑based plugin model, ease of customization, and out‑of‑the‑box features. Two Kong instances were run behind a cloud load balancer, forming a synchronized cluster.

Although the team considered a pure Python Thrift implementation (thriftpy) and the Motan framework from Weibo, limited manpower and varying skill levels led to the decision to develop the lightweight doge RPC solution instead.

4. Domain‑Driven Design

The next architectural step aimed to extract a dedicated data‑service layer. Each data service would host one or more bounded contexts, exposing a single aggregate root via RPC. Application services would depend on these data services but not vice‑versa, reducing coupling and allowing higher‑level services to remain independent of lower‑level implementations.

At the time of the author’s departure, DDD was still in the learning phase and had not been fully implemented, but the intention was to continue moving the backend toward this design.

Conclusion

Technology choices should serve product and customer needs rather than follow trends blindly; team composition and skill gaps inevitably force compromises, and the real challenge is finding optimal solutions within those constraints.

Emerging patterns such as Service Mesh are becoming mainstream, and the author plans to keep exploring them.

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.

BackendPythonMicroservicesDomain-Driven DesignDjangoKong
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

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.