Phase 4: Architecture & Scaling

Monolith-first approach & when to extract services

Advanced ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you want to build an amazing, huge Lego castle. When you first start, you probably don't know exactly what every room will look like, or if it will have a drawbridge or a secret dungeon. So, you just start building one big, connected castle. This is like a "monolith-first" idea in programming. You put everything important for your project – like the main entrance, the kitchen, the bedrooms, and the lookout tower – all together in one big Lego structure. It’s quick to build, easy to change if you decide the kitchen should be bigger, and you don’t have to worry about tiny separate pieces falling apart. This way, you can quickly see if your castle idea is fun and works well before spending ages planning every tiny detail.

The main reason you start this way is because it helps you build super fast. Maybe you just want to show your friends your cool new castle and see if they like playing with it. If they say, "Wow, this is amazing, but it really needs a dragon's cave!", you can easily add that cave right onto your existing big castle. You’re not trying to guess every single thing people might want from the start. You're focusing on getting the main fun parts ready first, and then you can add or change things as you learn more about what makes the castle truly awesome.

Now, imagine your big castle gets super popular, and lots of your friends want to play with it. One friend wants to play with the knights in the training yard, another wants to play with the queen in the throne room, and a third wants to control the guards on the wall. When everyone is trying to build or play with the exact same big castle at the same time, it can get tricky. Pieces might get mixed up, or someone might accidentally knock over another person's section. Or perhaps the dragon's cave is so popular that it keeps getting broken, but that doesn't stop the rest of the castle from being used. This is when you might decide it's time to "extract services."

You would carefully take the dragon's cave and build it as a completely separate, smaller Lego set. It still connects to the main castle story, but now your friends can play with it separately without disturbing the rest of the castle. You might do the same for the knight's training yard or the queen's throne room. You only break things out into separate, smaller builds when the big, single castle starts causing problems – like too many people trying to work on the same spot, or one part getting used so much it slows everything down for everyone else. So, when you're building your own cool projects, you can start with one big, easy-to-manage piece, and then break it into smaller, specialized parts only when it really helps make things better for everyone.

The monolith-first approach advocates starting new projects or products as a single, unified codebase, bundling all functionalities (user management, product catalog, order processing, etc.) into one deployable unit. This strategy prioritizes simplicity, rapid development, and reduced operational overhead in the early stages. For startups, MVPs, or projects where the business domain is still evolving and poorly understood, a monolith allows teams to quickly iterate, validate ideas, and pivot without the significant architectural complexity, distributed system challenges, and increased infrastructure costs inherent to microservices. It's about deferring complexity until it's absolutely necessary, focusing on delivering business value first.

The "when to extract" is crucial. You should only consider migrating functionality to a separate service when the monolithic architecture introduces tangible pain points that outweigh the overhead of distributed systems. Key triggers for extraction include: team scaling, where multiple independent teams struggle with code conflicts or slow deployment cycles due to a shared codebase; performance bottlenecks, where a specific module (e.g., a real-time analytics engine or a complex payment gateway) requires independent scaling or a specialized technology stack that's difficult to integrate into the monolith; and increasing complexity, where a module becomes so large and intertwined that development, testing, and maintenance become prohibitively slow and risky.

When these pain points arise and clear, stable domain boundaries become apparent, you can strategically extract a service. A common pattern for this is the "Strangler Fig Application," where new functionality or refactored existing functionality is implemented as a separate service, slowly siphoning traffic and responsibility away from the monolith. This gradual approach minimizes risk compared to a "big bang" rewrite. The extracted service should have a well-defined API contract with the remaining monolith, enabling independent development, deployment, and scaling while allowing the monolith to continue serving its remaining functions. This iterative extraction ensures you adopt microservices not out of hype, but out of necessity driven by real business and technical requirements.

Key Takeaways

  • Start with a monolith to defer complexity and accelerate initial development.
  • Extract services only when the monolith creates tangible pain points that hinder progress.
  • Key triggers for extraction: team scaling, performance bottlenecks, or excessive module complexity.
  • Identify clear, stable domain boundaries before attempting extraction.
  • Use patterns like the Strangler Fig Application for safe, gradual service extraction.

Code Example

python
# Monolithic App structure (conceptual Python)
class OrderProcessingModule:
    def create_order(self, user_id, items): pass # Order creation logic
    def update_order_status(self, order_id, status): pass # Status updates
    def process_payment(self, order_id, amount): pass # Financial processing (candidate for extraction)
    def send_order_confirmation(self, order_id, user_email): pass # Emailing (candidate for extraction)
    def notify_warehouse(self, order_id): pass # Inventory/Warehouse (candidate for extraction)

class MonolithicApp:
    def __init__(self):
        self.order_processor = OrderProcessingModule()

    def handle_new_order_request(self, request_data):
        # A single endpoint orchestrating many responsibilities of OrderProcessingModule
        order_id = self.order_processor.create_order(request_data['user_id'], request_data['items'])
        self.order_processor.process_payment(order_id, request_data['total'])
        self.order_processor.send_order_confirmation(order_id, request_data['email'])
        self.order_processor.notify_warehouse(order_id)
        return {"status": "order placed", "order_id": order_id}
# Observation: OrderProcessingModule is becoming a 'god object', handling too many distinct concerns. 
# 'process_payment', 'send_order_confirmation', 'notify_warehouse' are prime candidates for independent services.

How this code works

This Python code illustrates a "monolith-first" approach by showing how a single application handles all aspects of order processing. The OrderProcessingModule class centralizes various functions, from create_order to process_payment, send_order_confirmation, and notify_warehouse. The MonolithicApp then uses this module to manage a complete order lifecycle. Its handle_new_order_request method acts as a single point of entry, calling these functions one after another to process a new order from start to finish. This design keeps all related logic tightly coupled within one place, which is simple to start with.

The intent here is to demonstrate a single service performing many distinct jobs. For instance, after create_order, the code immediately calls process_payment, then send_order_confirmation, and finally notify_warehouse. A subtle point for beginners is that these are sequential, synchronous calls within the same application. If process_payment encounters an error, the subsequent steps like sending an email or notifying the warehouse might still be attempted, or the entire operation would fail synchronously at that point, requiring careful error handling within this one module. This bundling of diverse responsibilities makes the OrderProcessingModule a 'god object,' identifying its methods like process_payment and send_order_confirmation as prime candidates for later extraction into independent microservices due to their distinct concerns.