From Monolith to Modular Monolith on AWS App Runner

From Monolith to Modular Monolith on AWS App Runner

How to transform your monolithic applications into modular monoliths on AWS App Runner.
10 mins read
Jun 20, 2025
Share

Imagine you’ve poured countless hours into developing a fantastic application, starting with a streamlined monolithic architecture that quickly got you off the ground. Traffic starts to build, and you celebrate. But then, as your user base explodes, your once-nimble site slows to a crawl. Pages take too long to load, critical transactions fail, and users leave frustrated. The initial momentum fades almost as quickly as it came.

For many great business ideas that crash, the issue isn’t product or messaging: it’s infrastructure.

And while this kind of breakdown is preventable, it requires proactive planning.

While a traditional monolithic architecture offers simplicity initially, it often devolves into a tangled mess of dependencies, slow deployments, and painful scaling issues as your application grows. The dread of a single code change potentially bringing down the whole system. Scaling an entire application is frustrating because one small part is under load.

You’ve felt it, right?

But what if there was a way to get the best of both worlds—the relative simplicity of a monolith with the agility and scalability of microservices?

Enter the modular monolith. With AWS App Runner, deploying and managing this architectural patternIt is a reusable solution to a recurring problem in software architecture. has never been easier.

In this newsletter, we’ll explore how to transform your monolithic applications into modular monoliths on AWS App Runner, covering:

  • Architectural transformation: Understanding the modular monolith as the sweet spot.

  • Strategic module extraction: Practical methods for breaking down your codebase.

  • Effortless deployment and scaling: Leveraging AWS App Runner for seamless operations.

By the end, you’ll have a clear framework for building systems that can scale as fast as your ambitions, without the overwhelming complexity. Let’s get started.

Architectural transformation: From monolith to modular monolith#

The journey from a monolithic application to a modular monolith on AWS App Runner isn’t about throwing everything out and starting from scratch. It’s about strategic refactoring and leveraging the right tools to achieve a more maintainable, scalable, and resilient systemA resilient system continues to operate, even under adverse conditions or stress, and recovers to its effective operational posture in a timely manner..

You might be asking, “Why not just go full microservices?” While microservices offer ultimate decoupling and independent deployability, they introduce significant operational overhead: distributed transactions, complex communication patterns, and various services to manage. This complexity can outweigh the benefits for many teams, especially those starting their cloud journey or with smaller to medium-sized applications.

Monolith vs. modular monolith vs. microservices: A quick comparison

Feature

Traditional Monolith

Modular Monolith

Microservices

Deployment Unit

Single, large application

Single, large application

Multiple, independent services

Coupling

High, often tangled

Low within modules, high between modules

Loose, via APIs

Scaling

Scales the entire app

Scales the entire app, optimizes modules internally

Scales individual services

Complexity

Low initially, high later

Moderate

High

Communication

In-process calls

In-process calls, clear interfaces

Network calls (HTTP, RPC, messaging)

The modular monolith, on the other hand, provides a powerful middle ground, offering:

  • Simplified deployment: Still a single deployable unit, reducing the operational burden compared to managing dozens of individual microservices. This means fewer pipelines, less coordination, and easier rollbacks.

  • Clearer boundaries: Enforced modularity within the codebase, promoting independent development and reducing unintended side effects across different application parts. Developers can work on their modules with greater confidence.

  • Easier refactoring: Breaking down the monolith into well-defined modules makes future refactoring or eventual migration to true microservices much more manageable. Each module can be considered a candidate for extraction into its microservice.

  • Optimized performance: Modules within the same process can communicate directly, avoiding the network latency inherent in distributed systems. This often leads to better performance for tightly coupled functionalities.

Monolithic to modular monolithic architecture
Monolithic to modular monolithic architecture

This means that developers gain many of the benefits of microservices (modularity, independent development, clear ownership) without the immediate leap into distributed systems complexity. It’s a pragmatic approach to modernization that balances agility with manageability.

Visualizing a modular monolith#

To help visualize, imagine your application’s directory structure. Instead of a single, large codebase, a modular monolith might look something like this:

your-application/
├── src/
│ ├── modules/
│ │ ├── order/
│ │ │ ├── domain/
│ │ │ ├── infrastructure/
│ │ │ └── application/
│ │ ├── product/
│ │ │ ├── domain/
│ │ │ ├── infrastructure/
│ │ │ └── application/
│ │ └── user/
│ │ ├── domain/
│ │ ├── infrastructure/
│ │ └── application/
│ ├── shared/
│ │ └── utils/
│ └── main.py (or main.java, etc. - entry point for the single deployable unit)
├── tests/
└── Dockerfile

Each folder under modules/ represents a distinct, self-contained module. Communication between modules often happens through well-defined interfaces or in-memory messaging/event dispatchers to avoid tight coupling. This historical approach helps maintain separation while being part of a single process.

Strategic module extraction: The how-to guide#

The core of building a modular monolith lies in identifying and separating logical components within your existing codebase. This isn’t a “rip and replace” operation; it’s a careful, surgical procedure aiming to improve your architecture incrementally.

Here are some key strategies to guide your module extraction process:

  • Identify bounded contexts: Borrowing from domain-driven design (DDD)It is a software development approach to understand and model the business domain to create better software., identify distinct areas of your business logic that operate independently and have consistent language and rules. For example, in an e-commerce application, “Order Management”, “Product Catalog,” and “User Accounts” are likely separate bounded contexts. Each can form a natural module.

  • Separate by feature or business capability: Group related functionalities together. If a set of features always changes together, or if they represent a distinct business capability (e.g., “Payment Processing,” “Reporting”), they likely belong in the same module. This helps keep related logic co-located.

  • Analyze dependencies: Use static analysis tools to visualize your code’s dependencies. High-level, circular, or tangled dependencies between disparate parts of your application are red flags indicating potential module boundaries. The goal is to reduce these cross-module dependencies and create clear, one-way relationships where possible.

  • Create internal APIs/interfaces: Once you’ve identified a module, define clear interfaces or facades for how other application parts will interact. This enforces encapsulation and prevents direct, uncontrolled access to the module’s internal implementation details, much like a public API for a microservice. This is an application of the Facade patternThe Facade pattern is a software design pattern commonly used in object-oriented programming. Analogous to a façade in architecture, it is an object that serves as a front-facing interface, masking more complex underlying or structural code..

Example: Extracting a “Product” module#

Let’s imagine you have a monolithic ProductService class with many responsibilities.

The monolith: Before modularization

# app.py
class MonolithService:
def get_product(self, product_id):
# ... complex product retrieval logic ...
pass
def update_product_stock(self, product_id, quantity):
# ... stock update logic, potentially calling other parts of the monolith ...
pass
def process_order(self, order_details):
# ... order processing logic ...
pass
Monolith example code

The modular monolith: After refactoring

# modules/product/application/product_service.py
class ProductService:
def get_product(self, product_id):
# ... now only product retrieval logic ...
pass
def update_product_stock(self, product_id, quantity):
# ... now only stock update logic ...
pass
# app.py (main application entry point)
from modules.product.application.product_service import ProductService
class MainApplication:
def __init__(self):
self.product_service = ProductService() # Internal dependency injection
def handle_request(self, request):
if request.type == "get_product":
return self.product_service.get_product(request.id)
# ... other logic ...
Modular monolith example code

This simple example shows how ProductService now lives in its module, accessed through a clear interface. This is a common refactoring pattern known as the Extract moduleThe Extract module refactoring allows you to extract certain members from a selected class into a separate module..

Testing in a modular monolith#

Testing is crucial. For modular monoliths, adapt your practices:

  • Unit tests per module: Each module should have its comprehensive suite of unit tests, ensuring its internal logic works as expected.

  • Module-level integration tests: Test how components within a single module interact.

  • Interface/contract testing: Crucially, test the contracts (APIs/interfaces) that modules expose to each other. This ensures that changes in one module don’t break others, even if they’re in the same deployable unit.

Quick tip: Start small! Pick one or two well-defined, relatively independent modules to extract first. Learn from the process, identify what works and what doesn’t, and then apply those learnings to your application’s more complex or critical parts. Incremental change reduces risk. Modular monoliths can also benefit from targeted CI/CD workflows, where tools like Nx for JavaScript or Gradle composite builds for Java can be configured to only run tests or builds for changed modules, speeding up your development cycle.

Effortless deployment and scaling with AWS App Runner#

AWS App Runner is a fully managed service that makes deploying containerized web applications and APIs incredibly easy. It’s perfect for modular monoliths because it handles infrastructure provisioning, scaling, and load balancing, allowing you to focus on your code and modular design rather than operational complexities.

Here’s how App Runner simplifies your deployment and operations:

  • Container-native deployment: Package your modular monolith as a Docker image. App Runner can build directly from your source code repository (e.g., GitHub, Bitbucket) or an existing Amazon Elastic Container Registry (ECR) image. This containerization provides a consistent environment from development to production.

  • Automatic scaling: App Runner automatically scales your application up and down based on incoming traffic. This means you only pay for the resources you use, eliminating idle capacity costs, and your application can handle unpredictable traffic spikes without any manual intervention. This is crucial for maintaining performance during viral campaigns.

  • Integrated load balancing and HTTPS: App Runner automatically provides a secure load balancer and handles HTTPS termination, simplifying your network configuration and ensuring secure communication by default. You don’t need to configure separate load balancers or SSL certificates.

  • Simplified CI/CD: App Runner integrates seamlessly with CI/CD pipelines. You can configure it to automatically deploy new versions of your application whenever you push new code to your connected repository. This enables rapid, continuous delivery of updates and features.

AWS AppRunner deployment pipeline
AWS AppRunner deployment pipeline

Example scenario: Imagine your e-commerce application has been refactored into distinct internal modules for “Products”, “Orders,” and “User Accounts.” You can containerize this modular monolith. When you push a change to your “Products” module (part of the larger container image), App Runner can detect the change, rebuild, and deploy the entire application. The internal modularity you’ve built into the codebase ensures that the changes within the “Products” module are isolated and tested within its boundaries, reducing the risk of unintended impacts on other parts of the system, despite being deployed as a single unit.

App Runner vs. ECS vs. Kubernetes#

While App Runner is fantastic for many use cases, AWS offers other container services like Amazon Elastic Container Service (ECS) and Amazon Elastic Kubernetes Service (EKS—a managed Kubernetes service). Here’s a quick comparison:

Feature

AWS App Runner

Amazon ECS

Amazon EKS (Managed Kubernetes)

Management Burden

Fully managed, minimal operational overhead

Requires cluster management, but simpler than EKS

High operational overhead, full Kubernetes control

Ideal Use Case

Web apps, APIs, simple services, rapid deployment

Complex microservices, custom networking, and hybrid setups

Highly customized, large-scale, complex orchestrations

Cost Model

Pay per request and compute, no idle container fees

Pay for EC2 instances/Fargate tasks

Pay for control plane + EC2 instances/Fargate tasks

Complexity

Low

Moderate

High

Amazon recommends starting with App Runner for a modular monolith architecture, emphasizing ease of deployment and automatic scaling. If your application requirements grow to include complex orchestration, custom networking, or advanced Kubernetes capabilities, then Amazon ECS or EKS may be more appropriate.

Concrete examples for each service:

  • App Runner: A startup building their first e-commerce application, focusing on rapid iteration and automatic scaling for their modular monolith.

  • ECS: A mature company with multiple microservices, needing fine-grained control over networking and service discovery, but not the full complexity of Kubernetes.

  • EKS: A large enterprise with a dedicated DevOps team, requiring maximum control over container orchestration, potentially across on-premises and cloud environments, and leveraging existing Kubernetes expertise.

Sample App Runner configuration#

To illustrate how simple it is to deploy your modular monolith with App Runner, here’s a basic apprunner.yaml file you might use for source code deployments:

# apprunner.yaml (place at the root of your repository)
version: 1.0
runtime: python3
build:
commands:
build:
- pip install -r requirements.txt
run:
command: python app.py
port: 8080
# If you need environment variables for different modules (e.g., database connection strings)
# you would define them here or through the AppRunner console.
# env:
# - name: MODULE_ORDER_DB_HOST
# value: your-order-db.rds.amazonaws.com

This file tells App Runner how to build and run your application. You’d typically package your entire modular monolith within this single Docker image.

Observability with App Runner#

To gain visibility into your modular monolith’s performance and errors running on App Runner:

  • AWS CloudWatch logs: App Runner automatically sends application logs to CloudWatch Logs. You can create log groups and analyze individual module logs for errors or performance issues.

  • AWS CloudWatch: Monitor key metrics like CPU utilization, memory usage, and request counts. You can set up alarms to notify you of atypical behavior.

  • AWS X-Ray: For more granular tracingIt refers to collecting and analyzing detailed information about the execution of a process or program. , instrument your application with X-Ray. This lets you visualize requests flowing through your modular monolith, helping you identify performance bottlenecksPerformance bottlenecks are points in a system (hardware, software, or network) that restrict overall throughput or speed. within specific modules. You can trace calls between modules using internal messaging queues or custom instrumentation.

Modular design also supports targeted optimization in App Runner. By observing which modules are responsible for resource spikes through CloudWatch and X-Ray, you can prioritize refactoring or optimizing those specific areas, rather than optimizing the entire application.

Embracing modular growth#

The shift from a monolithic architecture to a modular monolith on AWS App Runner is a powerful and pragmatic strategy for modernizing your applications. It offers a clear, practical path to increased agility, improved scalability, and reduced operational overheadIt refers to the ongoing expenses a business incurs to keep its doors open and operate, regardless of whether it’s producing goods or services., without the immediate complexities and distributed systems challenges of a full microservices architecture.

You can get hands-on with AWS App Runner (no AWS account needed) with this Cloud Lab: Getting Started with AWS App Runner

By embracing modularity, you create a codebase that is easier to understand, maintain, and evolve, setting the stage for sustainable growth.

Final takeaways: To build more resilient, scalable, and manageable applications, focus on:

  • Strategic modularity: Identify and encapsulate distinct business logic into well-defined, cohesive modules.

  • Clear interfaces: Use well-defined internal APIs to enforce strong boundaries between your modules, treating them like mini-services within your application.

  • Leverage managed services: Utilize powerful services like AWS App Runner to offload operational burdens, automate scaling, and focus your team's efforts on delivering core business value.


Written By:
Fahim ul Haq
Free Edition
Protect your applications using AWS WAF
Learn how AWS WAF moves application security from a performance trade-off to an enabler of business resilience.
10 mins read
Jan 30, 2026