ArchitectMind
  1. Home
  2. Tutorials
  3. Book 7 · Application Architecture & Distributed Systems
  4. Volume II
  5. Monolith Fundamentals
Book 7 · Volume II — Application Architecture Styles & Patterns · Topic 1.1

Monolith Fundamentals

Single deployable unit, internal modules and shared runtime

19 min readStage 3 · Systems8 interview questions
Chapter
Chapter 1 — Monolithic Architecture
Prerequisites
Volume I
You will be able to
Define a monolith by its deployment unit rather than its code quality, describe the internal module structure of a well-built monolith, explain what "shared runtime" means for memory, threads, connections, classpath and failure, reason about in-process versus network interactions, and recognise the architectural consequences of choosing a monolith
Covers
Single deployable unit; Internal modules; Shared runtime
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — In-Process Interaction
  5. 4A. Deep Dive — Isolation Inside a Shared Runtime
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

A monolith is an application that is built, deployed and run as a single unit: one artefact, one process (possibly replicated), usually one database. Internally it may be well structured or not — "monolith" describes deployment topology, not quality.

Single deployable unit (TOC)
Figure 1 — Single deployable unit (TOC)
TOC conceptDefinition
Single deployable unitAll functionality ships in one artefact (JAR, WAR, container image) with one version and one release; replicas are identical copies
Internal modulesThe logical parts inside the deployable — packages, modules, components — which may have clear boundaries (modular) or none (mud)
Shared runtimeAll modules run in the same process: one JVM heap, thread pools, connection pool, classpath, configuration and lifecycle
Table 1 — The three TOC concepts

2Why It Exists

A monolith is the natural starting point: one codebase, one build, one deployment, local calls and local transactions. For many teams and domains, its simplicity outweighs the independence that distribution provides.

Monolith vs distributed system
Figure 2 — Monolith vs distributed system

3Core Concepts

3.1 Single deployable unit

DOCKERFILE
# One artefact → one image → N identical replicas
FROM eclipse-temurin:25-jre
COPY target/shop-2026.3.1.jar /app/shop.jar
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/shop.jar"]
PropertyConsequence
One version numberAll features release together
One pipelineSimple CI/CD
Identical replicasHorizontal scaling = more copies of everything
One startupStartup time grows with size
Table 2 — What "single deployable" implies

3.2 Internal modules

Internal modules (TOC)
Figure 3 — Internal modules (TOC)
JAVA
// Spring Modulith: modules are top-level packages; verify() enforces boundaries
com.shop.catalog      // CatalogApi + internal
com.shop.pricing      // PricingApi + internal
com.shop.ordering     // uses PricingApi, publishes OrderPlaced
com.shop.payments     // listens to OrderPlaced
 
@Test
void verifyModules() { ApplicationModules.of(ShopApplication.class).verify(); }

3.3 Shared runtime

Shared runtime (TOC)
Figure 4 — Shared runtime (TOC)
Shared resourceRiskMitigation
HeapOne module's leak kills allMemory profiling, limits per cache
Thread poolsSlow module starves othersBulkheads, separate executors, virtual threads
DB connection poolBatch job exhausts poolSeparate pool for batch, limits
ClasspathLibrary conflictsDependency management, BOMs
Process fateCrash = total outageReplicas, health checks
Table 3 — Shared runtime trade-offs

4How It Works Internally — In-Process Interaction

A request inside a monolith
Figure 5 — A request inside a monolith
Call cost: in-process vs network
Figure 6 — Call cost: in-process vs network
InteractionMonolithDistributed
CallMethod callHTTP/gRPC/message
Failure modesExceptionsTimeouts, partial failure
TransactionLocal ACIDSaga/outbox
Contract changeCompiler checksVersioning, contract tests
Table 4 — Interaction differences

4ADeep Dive — Isolation Inside a Shared Runtime

JAVA
// Bulkhead: give the slow reporting module its own bounded executor
@Bean(name = "reportingExecutor")
Executor reportingExecutor() {
    return new ThreadPoolTaskExecutorBuilder().corePoolSize(4).maxPoolSize(4)
            .queueCapacity(100).threadNamePrefix("reporting-").build();
}
 
@Async("reportingExecutor")
public CompletableFuture<Report> generate(ReportRequest r) { … }
YAML
# Separate connection pool for batch/reporting so OLTP is protected
reporting:
  datasource:
    url: jdbc:postgresql://db/shop
    hikari: { maximum-pool-size: 5, pool-name: reporting }
What a monolith shares
Figure 7 — What a monolith shares

5APIs / Features / Syntax

ToolMonolith use
Spring Boot 4 executable JAR / BuildpacksSingle artefact
Spring ModulithModules, events, verification, docs
Virtual threadsHigh concurrency in one process
Micrometer per-module metrics (@Observed)Visibility inside the monolith
ArchUnitStructure rules
Kubernetes Deployment + HPAReplicas of the whole app
Table 5 — Toolkit

6Practical Examples

6.1 Successful monolith at scale

An Indian e-commerce company ran checkout as a modular Spring monolith handling 3,000 orders/min with 25 engineers; releases went out daily. Only search was extracted, for independent scaling.

6.2 Shared runtime incident

A reporting feature loaded a year of orders into memory; the JVM ran out of heap and every instance crashed, taking down checkout. Moving reports to a separate executor, streaming queries and a read replica isolated the risk.

6.3 Worked example — deployable inventory

ArtefactContents
shop.jarcatalog, pricing, ordering, payments, admin
DatabasePostgreSQL with 5 schemas
Replicas4 pods behind ALB
Table 6 — A typical monolith

Scenario. A new architect joined ShopNow and was told the shop "is a monolith". Before forming any opinion, she listed exactly what was deployed and how.

Walkthrough. The inventory shows one artefact, shop.jar, containing the catalog, pricing, ordering, payments and admin modules. It runs as four identical pods behind an AWS Application Load Balancer, all connected to one PostgreSQL database with five schemas, one per module. Every release builds and deploys the whole jar, and every pod contains every module.

Result. The inventory made the system's real properties visible: simple deployment and local transactions, but shared release cadence, shared scaling and a shared failure domain. It also showed that schemas were already separated per module, a good starting point for clearer boundaries.

Takeaway. Describe a monolith by its deployable units, runtime instances and data stores before judging it. "Monolith" describes deployment, not quality, and the details decide which improvements make sense.

6.4 Worked example — module API

JAVA
package com.shop.pricing;
public interface PricingApi {
    Quote quote(QuoteRequest request);
}

Scenario. The ordering module in ShopNow's monolith called pricing's internal classes directly, including a repository and several helper classes. Every pricing refactoring broke ordering.

Walkthrough. The pricing team introduced a single public interface, PricingApi, in the module's root package, exposing one operation: quote a price for a request. Everything else moved into internal sub-packages. Spring Modulith treats the root package as the module's API and flags any access to internal packages from other modules as a violation in its verification test.

Result. Ordering now depends only on PricingApi and its Quote and QuoteRequest types. Pricing can restructure its internals freely, and the dependency is small enough that pricing could later become a separate service by replacing the in-process implementation with a remote client.

Takeaway. Give each module in a monolith a small, explicit API and hide everything else. A module boundary is only real when other modules cannot reach past it.

6.5 Worked example — in-process event

JAVA
@ApplicationModuleListener
void on(OrderPlaced e) { payments.authorise(e.orderId(), e.total()); }

Scenario. When an order was placed, the ordering module called payments, loyalty, notifications and analytics directly. Ordering had to know about all of them, and a failure in analytics could break order placement.

Walkthrough. Ordering now publishes an OrderPlaced event inside its transaction, and interested modules react to it. Spring Modulith's @ApplicationModuleListener runs the listener asynchronously after the publishing transaction commits, in its own transaction, and records the event in an event publication registry so it can be retried if the listener fails. Here, payments authorises the payment for the order when it receives the event.

Result. Ordering no longer depends on its downstream modules, new reactions can be added without changing ordering, and a failure in one listener does not roll back the order. The same events can later be externalised to Kafka if modules are extracted.

Takeaway. Use in-process events to decouple modules inside a monolith. Asynchronous, transactional listeners give much of the decoupling of messaging without the operational cost of a broker.

6.6 Worked example — scaling math

LoadPods
Normal (500 rps)3
Sale (3,000 rps)12 (each with all modules)
Table 7 — Scaling multiplies everything

Scenario. During ShopNow's annual sale, traffic rose from 500 to 3,000 requests per second. Only the catalog and checkout were under real pressure, yet the infrastructure bill quadrupled.

Walkthrough. A monolith scales by running more copies of the whole application. Normal traffic needs three pods; the sale needs twelve. Each pod contains every module, so admin, reporting and every other rarely used module were also replicated twelve times, each copy consuming memory, database connections and start-up time.

Result. The cost was acceptable for a few days a year, so the team kept the monolith but added a cache for catalog reads, which reduced the pods needed at peak to eight. Only if one module's load profile diverged permanently would extraction be worth it.

Takeaway. Scaling a monolith multiplies everything, so scale costs grow with the whole application's footprint. First reduce load on hot paths, with caching or read models, before considering extraction.

6.7 Worked example — startup time

SizeStartup
Small4 s
Large (400 beans+)45 s
With CDS/AOT~15 s
Table 8 — Big monoliths need startup optimisation

Scenario. As ShopNow's monolith grew past 400 beans, start-up time climbed to 45 seconds. Rolling deployments became slow, and autoscaling reacted too late during traffic spikes.

Walkthrough. Start-up time grows with the number of classes, beans and eager initialisation. The team measured where time went, made rarely used beans lazy, removed unnecessary classpath scanning and enabled class-data sharing, and then the AOT cache of recent JDKs, so that class loading and linking work is reused across runs.

BASH
# Training run: record classes and profiles into an AOT cache (JDK 25)
java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar shop.jar
# Production: start with the cache
java -XX:AOTCache=app.aot -jar shop.jar

Result. Start-up fell to about 15 seconds. Deployments finished in a third of the time, and new pods joined during traffic spikes before latency degraded.

Takeaway. Large monoliths need deliberate start-up optimisation. Measure first, then reduce eager work and reuse start-up work across runs with CDS or AOT caches.

6.8 Worked example — module-level observability

JAVA
@Observed(name = "pricing.quote", contextualName = "pricing-quote")
public Quote quote(QuoteRequest r) { … }

Scenario. ShopNow's monolith dashboards showed only overall latency and error rates. When checkout slowed down, nobody could tell whether pricing, payments or inventory was responsible.

Walkthrough. Adding Micrometer's @Observed to each module's API methods, such as pricing's quote method, creates a timer metric and a tracing span named after the module operation. Dashboards can now show latency and errors per module operation, and traces show how much of each request is spent in each module, even though everything runs in one process.

Result. The next checkout slowdown was traced in minutes to pricing's quote call, which had started loading promotions from the database on every request. A cache fixed it.

Takeaway. Observe a monolith at module level, not only as a whole. Module-level metrics and spans make a monolith as diagnosable as a set of services.

6.9 Worked example — failure blast radius

FailureImpact
OOM in reportingAll modules down
Pricing bugCheckout affected
Table 9 — Fate sharing

Scenario. A finance user started a large report export that loaded millions of rows into memory. The JVM ran out of memory, and every pod serving that request crashed, taking checkout down with it.

Walkthrough. In a monolith all modules share one process, so they share its fate: an out-of-memory error, a thread leak or a crashing native library in one module stops every module in that instance. Similarly, a bug in the pricing module directly affects checkout, because checkout calls it in-process.

Result. The team moved report generation to a separate deployment of the same code, running only the reporting profile, and added memory limits and streaming to the export. Checkout was no longer exposed to reporting's resource use.

Takeaway. Remember that modules in a monolith share a failure domain. Isolate resource-heavy or risky workloads into separate processes or deployments, even if they share the code base.

6.10 Worked example — one database, many schemas

SchemaOwner module
catalogCatalog
orderingOrdering
Table 10 — Ownership inside one DB

Scenario. ShopNow's monolith used one PostgreSQL database. Without rules, every module queried every table, so any schema change could break code anywhere in the application.

Walkthrough. Each module now owns its own schema: the catalog module owns the catalog schema, ordering owns ordering, and so on. Each module's code accesses only its own schema, enforced by tests that check repository packages and by separate database roles in production. Data owned by another module is obtained through that module's API or events.

Result. Schema changes became local decisions of the owning module. When ordering was later extracted into a service, its schema moved to a new database with almost no changes to other modules.

Takeaway. Apply data ownership inside the monolith with one schema per module. It costs little now and makes later extraction, or simply safe change, far easier.

6.11 Worked example — health checks

ProbeMeaning
livenessprocess alive
readinessDB reachable, ready for traffic
Table 11 — Whole-app health

Scenario. A database outage made every monolith pod fail its liveness probe. Kubernetes restarted all pods repeatedly, which turned a short database blip into a long, noisy outage.

Walkthrough. Liveness and readiness answer different questions. Liveness asks whether the process is alive and able to make progress; it should not depend on external systems, or a database failure will cause restart storms. Readiness asks whether the instance can serve traffic now, and it can include critical dependencies such as the database, so traffic is withheld until they recover.

YAML
management:
  endpoint:
    health:
      probes: { enabled: true }
      group:
        liveness:  { include: livenessState }
        readiness: { include: readinessState, db }

Result. During the next database blip, pods stayed running but were marked not ready; traffic paused briefly and resumed as soon as the database returned, without restarts.

Takeaway. Keep liveness probes about the process itself and put dependencies into readiness. Badly designed health checks can turn small incidents into large ones.

6.12 Worked example — monolith vs "distributed monolith"

PropertyMonolithDistributed monolith
Deploy togetherYes (one unit)Yes (many units!)
Network callsNoYes
Table 12 — The worst of both is distribution without independence

Scenario. ShopNow split its monolith into eight services, but every release still required deploying several of them together, in a specific order, with a shared database.

Walkthrough. Compared property by property, the new system was worse than the monolith it replaced. It still deployed together, so it had no release independence, but it now made network calls between parts, adding latency, partial failures and operational complexity. This is a distributed monolith: distribution without independence.

Result. The team merged the tightly coupled services back into two deployable units with clear module boundaries, and extracted only one service, payments, which genuinely needed independence.

Takeaway. Distribution only pays off when it brings independent deployment and change. If services must still be released together, a well-modularised monolith is usually the better choice.

6.13 Worked example — horizontal scaling requirements

RequirementWhy
Stateless web tierAny pod serves any request
Sessions in Redis/JWTNo sticky sessions
Table 13 — Monoliths scale horizontally too

Scenario. ShopNow's monolith originally stored HTTP sessions in memory and relied on sticky load-balancer sessions. Scaling out caused users to lose their carts whenever a pod was replaced.

Walkthrough. To scale horizontally, any pod must be able to handle any request. The web tier was made stateless: sessions moved to Redis through Spring Session, and API clients use JWTs that carry identity in each request. Caches either moved to Redis or became safe to lose. Sticky sessions were removed from the load balancer.

Result. Pods can now be added, removed and replaced freely during scaling and deployments, and users no longer lose carts. The monolith scales horizontally like any service.

Takeaway. Monoliths scale horizontally too, as long as instances hold no user state. Externalise sessions and shared caches before relying on scaling out.

6.14 Worked example — configuration

ConfigScope
application.yml with module sectionsOne config for all
Feature flagsToggle module features
Table 14 — Shared configuration

Scenario. A feature in ShopNow's monolith needed to be enabled for the marketing team before the rest of the release was ready, but all modules shared one release and one configuration file.

Walkthrough. Configuration in a monolith is shared: one application.yml with a section per module, overridden per environment. Feature flags add finer control, so a module's new behaviour can be deployed switched off and enabled independently of the release. The team used a flag for the new promotion engine, with a property per module feature and a flag service for runtime changes.

YAML
pricing:
  promotions:
    engine: v2          # module section
features:
  pricing.promotion-engine-v2: false   # enabled later via the flag service

Result. The code shipped with the regular release, and marketing enabled the feature a week later without a deployment. Rolling back was also a flag change.

Takeaway. Organise shared configuration by module and use feature flags to decouple release from deployment. Flags give monolith teams some of the independence services offer.

6.15 Worked example — team structure

TeamsModel
1–3 teamsModule ownership inside one repo
> 5 teamsConsider extraction where contention appears
Table 15 — Team fit

Scenario. ShopNow's single monolith was developed by two teams in its early years. As the company grew to seven teams, merge conflicts, queueing for releases and unclear ownership slowed everyone down.

Walkthrough. The table summarises the fit. With one to three teams, a monolith with clear module ownership inside one repository works well: coordination is cheap and refactoring is easy. With more than about five teams, contention appears in the shared code base, build and release process. The team identified where contention was worst, payments and catalog, and extracted only those modules into services with their own teams.

Result. Release queues shortened, and the remaining modules continued in the monolith with clear owners. The extraction was driven by measured team contention rather than by fashion.

Takeaway. Match the architecture to team structure. Extract services where team contention is real and measured, not as a general policy once a team count is reached.

6.16 Worked example — dependency management

XML
<dependencyManagement>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>4.0.0</version><type>pom</type><scope>import</scope></dependency>
  </dependencies>
</dependencyManagement>

Scenario. Over the years, ShopNow's monolith accumulated conflicting library versions: two JSON libraries, three versions of a logging bridge and incompatible Spring modules, which caused subtle runtime errors.

Walkthrough. A monolith has one classpath, so every module must agree on library versions. Importing Spring Boot's dependency BOM in dependencyManagement aligns hundreds of library versions to a tested set. Modules then declare dependencies without versions, and the build fails on conflicting versions through Maven Enforcer rules.

Result. Version conflicts disappeared, and upgrades became one change to the Boot version, tested once for the whole application. Dependency updates were automated with Renovate.

Takeaway. In a monolith, dependency management is a shared responsibility. Use a single BOM, ban per-module version overrides and automate upgrades to keep the classpath consistent.

6.17 Worked example — when a module needs isolation

SignalAction
Different scaling profileExtract or separate read model
Different release cadenceExtract
Resource hogBulkhead or separate process
Table 16 — Isolation options

Scenario. ShopNow's monolith had three modules with very different needs: catalog search with high read load, a reporting module that consumed large amounts of memory, and a pricing module that needed releases several times a day.

Walkthrough. The table matches each signal to an isolation option rather than assuming extraction. Catalog's different scaling profile was handled with a separate read model and cache. Reporting, a resource hog, ran in a separate process from the same code base, acting as a bulkhead. Only pricing, with its different release cadence, was extracted as a service with its own pipeline.

Result. Each problem was solved with the lightest isolation that worked. Only one new service was created, and the monolith stayed simpler to operate than a full microservice split.

Takeaway. When a module needs isolation, choose the lightest option that fits the signal: caching, separate processes or extraction. Extraction is the most expensive option, not the default.

6.18 Worked example — interview answer

PointAnswer
DefinitionSingle deployable unit
StructureInternal modules (ideally enforced)
RuntimeShared process & resources
ConsequenceSimple, but fate- and scale-coupled
Table 17 — Structured answer

Scenario. In an interview, a candidate was asked to explain what a monolith is and whether it is bad. Many candidates answered with opinions rather than properties.

How to answer. A structured answer covers four points. Definition: an application built and deployed as a single unit. Structure: internally it should consist of modules with enforced boundaries. Runtime: all modules share one process, with its memory, threads and connection pools. Consequence: it is simple to develop, test and deploy, with local transactions, but modules share their fate, their scaling and their release cadence.

How to finish. Ending with when you would choose a monolith, for example a modular monolith for a small or medium team with an evolving domain, shows judgement rather than ideology.

Takeaway. Answer architecture questions with properties and trade-offs. Interviewers look for understanding of consequences, not a verdict on whether monoliths are good or bad.

6.19 Worked example — monolith runtime sizing

ResourceSettingReason
Heap-XX:MaxRAMPercentage=75 on 2 GiB podsContainer-aware JVM
HTTP concurrencyVirtual threadsBlocking I/O without huge pools
OLTP poolHikari 20 per podDB max_connections budget
Reporting pool5 per podProtect OLTP
Pods3–12 (HPA on CPU/RPS)Scale replicas
Table 18 — Explicit resource budget for a shared runtime

Scenario. ShopNow's monolith ran in Kubernetes pods with default settings. Under load, pods were OOM-killed, request threads ran out and the database refused connections.

Walkthrough. The team sized each runtime resource deliberately. The heap uses -XX:MaxRAMPercentage=75 on 2 GiB pods, so the JVM respects container limits and leaves room for non-heap memory. Virtual threads handle blocking I/O without large platform thread pools. Each pod has a HikariCP pool of 20 connections, chosen so that pods at peak stay within PostgreSQL's max_connections.

YAML
spring:
  threads.virtual.enabled: true
  datasource.hikari:
    maximum-pool-size: 20
    connection-timeout: 2s
# JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=75 (2 GiB pod limit)

Result. OOM kills stopped, the service handled peak traffic with fewer pods, and database connection errors disappeared.

Takeaway. Size a monolith's runtime as a whole: memory, threads and connections must fit together and fit the shared database. Defaults are rarely right for production.

6.20 Worked example — module dependency view

TEXT
catalog   ← pricing ← ordering → payments (events)
                         ↓
                      shipping (events)
RuleCheck
No cyclesverify()
Only API packages usedModulith named interfaces
Events for side effectsCode review + Modulith docs
Table 19 — Module graph as part of the monolith's architecture

Scenario. ShopNow's architects wanted to see, at a glance, how modules in the monolith depended on each other and whether that matched the intended design.

Walkthrough. The module dependency view shows catalog used by pricing, pricing used by ordering, and ordering notifying payments and shipping through events rather than direct calls. Two checks keep it true: Spring Modulith's verify() fails the build on any dependency cycle, and named interfaces restrict which packages of a module others may use.

Result. When a developer added a call from catalog to ordering, the build failed with a cycle error, and the team instead published a catalog event. The dependency view remained accurate.

Takeaway. Keep a simple dependency view of your modules and enforce it with verification. Direction of dependencies is as important as their existence.

6.21 Worked example — database connection budget

ItemValue
PostgreSQL max_connections400
Pods at peak12
Per pod (OLTP + reporting)25
Total300 (+ headroom for admin/CDC)
Table 20 — Shared database needs capacity planning

Scenario. After scaling the monolith to 12 pods for a sale, ShopNow's database started rejecting connections, although each pod's pool was modest.

Walkthrough. Connections must be budgeted across all instances. PostgreSQL allows 400 connections. At peak, 12 pods each use up to 25 connections, 20 for transactions and 5 for reporting, for a total of 300. The remaining headroom covers administrators, migrations and change-data-capture tools. Before the fix, pods used the default pool size plus a separate reporting pool, exceeding the limit at peak.

Result. With explicit budgets, the sale ran without connection errors, and the team added a PgBouncer connection pooler as the next step to support further scaling.

Takeaway. A shared database needs capacity planning across all instances: multiply pool sizes by the maximum number of pods and keep headroom. Autoscaling without a connection budget will eventually exhaust the database.

7Common Problems & Mistakes

  • Equating monolith with bad design.
  • No internal module boundaries.
  • No isolation of resource-hungry modules.
  • Stateful instances blocking horizontal scaling.
  • Ignoring startup time growth.
  • Distributing prematurely into a distributed monolith.

8Performance & Production Considerations

AspectGuideline
StructureEnforced modules (Modulith)
RuntimeBulkheads, separate pools for heavy work
ScalingStateless, replicas, HPA
StartupCDS/AOT, lazy init where safe
ObservabilityPer-module metrics and traces
Table 21 — Operational guidelines

9Architecture & Design Considerations

  • Choose a monolith for simplicity, then keep it modular.
  • Isolate resource-intensive modules within the runtime.
  • Keep instances stateless to scale horizontally.
  • Treat modules as future service candidates.

10Interview Questions & Answers

Q1Basic

What is a monolith?

A monolith is an application that is built, deployed and run as a single unit. All its functionality, for example catalog, pricing, ordering and payments, is packaged together, often as one jar or war, and released together. At run time it typically executes as one process per instance, and instances are identical copies. A monolith often uses one database, ideally with clear ownership of tables or schemas by modules. The term describes how the software is deployed, not how well it is structured: a monolith can be cleanly modular or a tangled big ball of mud.

Q2Basic

Does monolith mean badly designed?

No. "Monolith" describes the deployment model, a single deployable unit, not the quality of the design. A monolith can be well designed, with clear modules, enforced boundaries, explicit APIs and data ownership per module; this is called a modular monolith. Many successful systems run this way for years because it is simpler to develop, test, deploy and operate than a distributed system. The bad reputation comes from monoliths without internal structure, where everything depends on everything. The real question is whether the internal boundaries are clear, not whether the system is deployed as one unit.

Q3Intermediate

What does shared runtime mean?

All modules run inside the same operating-system process and share its resources: the heap and memory, the threads and thread pools, database connection pools, the classpath with one version of each library, configuration, and the application's life cycle, so they start, stop and are deployed together. Shared runtime makes communication cheap, using method calls and local transactions, but it also means modules share their fate. A memory leak or thread exhaustion in one module affects all of them, and every module scales and restarts together. Bulkheads and resource limits can reduce, but not remove, that coupling.

Q4Intermediate

How do modules communicate in a monolith?

Modules communicate in-process. The most direct way is a method call on another module's public API, such as an interface like PricingApi, which is fast and type-checked. Modules can also publish in-process events, for example with Spring application events or Spring Modulith's event publication, so the publisher does not depend on the consumers. Both can participate in local database transactions, giving strong consistency without distributed transactions. To keep modules decoupled, calls should use only published API types, never internal classes, and modules should not query each other's tables directly.

Q5Senior Engineer

How do you prevent one module from starving others?

Use bulkheads to separate shared resources. Give resource-intensive modules their own executors or thread pools and their own database connection pools, so they cannot exhaust capacity needed by others. Apply limits, such as timeouts, maximum result sizes and rate limits on expensive operations. Use streaming instead of loading large data sets into memory. Monitor per-module metrics to find which module is consuming resources, and profile under realistic load. When a module's resource use is fundamentally different, as with heavy reporting, run it in a separate process or deployment, even if the code base stays shared.

Q6Senior Engineer

How does a monolith scale?

A monolith scales horizontally by running more identical instances behind a load balancer, such as additional Kubernetes pods. For this to work, instances must be stateless: user sessions are stored in Redis or carried in tokens, caches are shared or safe to lose, and scheduled jobs run once rather than on every instance. It can also scale vertically with larger instances, and read load can be reduced with caching, read replicas or separate read models. The limitation is that every instance contains every module, so the whole application scales together, and the shared database must be sized for the total.

Q7Technical Architect

What is a distributed monolith?

A distributed monolith is a system split into multiple services that still cannot be changed or deployed independently. Typical causes are shared databases, shared domain libraries, chatty synchronous calls and API changes that require coordinated releases. It combines the downsides of both worlds: the coordination and release coupling of a monolith with the latency, partial failures and operational overhead of a distributed system. It often results from splitting a system by technical layers or too early, before boundaries are understood. The fix is either to consolidate tightly coupled services or to establish true independence.

Q8Solution Architect

When would you recommend a monolith?

Recommend a monolith, ideally a modular monolith, when the team is small to medium, the domain is still evolving so boundaries are uncertain, and strong consistency through local transactions is valuable. It is also a good choice when operational maturity for distributed systems, such as automated delivery and observability, is limited. Design it with clear modules, explicit APIs, data ownership per module and enforced boundaries, so that individual modules can be extracted later when there is a concrete need, such as independent scaling, a separate team or a different release cadence.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 7

Application Architecture & Distributed Systems

Volume I · Topic 1.1

Software Architecture

Architecture Fundamentals & Architectural Thinking

Volume I · Topic 1.2

Architecture Responsibilities

Architecture Fundamentals & Architectural Thinking

Volume I · Topic 1.3

Architect vs Developer

Architecture Fundamentals & Architectural Thinking

Volume II · Topic 1.2

Types of Monoliths

Application Architecture Styles & Patterns

Volume II · Topic 1.3

Monolith Advantages

Application Architecture Styles & Patterns

Volume III · Topic 1.1

Domain-Driven Design

Domain-Driven Design & Business Boundaries

Volume III · Topic 1.2

Strategic vs Tactical DDD

Domain-Driven Design & Business Boundaries

Volume III · Topic 1.3

Domain Complexity

Domain-Driven Design & Business Boundaries

Volume IV · Topic 1.1

What Is a Distributed System?

Distributed Systems Fundamentals

Volume IV · Topic 1.2

Why Distributed Systems Exist

Distributed Systems Fundamentals

Volume IV · Topic 1.3

Distributed System Complexity

Distributed Systems Fundamentals

Volume V · Topic 1.1

What Makes a Microservice?

Microservices Architecture

Volume V · Topic 1.2

Service Granularity

Microservices Architecture

Volume V · Topic 1.3

Microservice Boundaries

Microservices Architecture

Volume VI · Topic 1.1

Event Fundamentals

Event-Driven & Asynchronous Architecture

Volume VI · Topic 1.2

Event Types

Event-Driven & Asynchronous Architecture

Volume VI · Topic 1.3

Event-Carried State

Event-Driven & Asynchronous Architecture

Volume VII · Topic 1.1

Scalability

Resilience, Scalability & Distributed Data

Volume VII · Topic 1.2

Stateless Architecture

Resilience, Scalability & Distributed Data

Volume VII · Topic 1.3

Load Balancing

Resilience, Scalability & Distributed Data

Volume VIII · Topic 1.1

ADR

Architecture Decision-Making & System Evolution

Volume VIII · Topic 1.2

Architecture Decision Process

Architecture Decision-Making & System Evolution

Volume VIII · Topic 1.3

Reversible vs Irreversible Decisions

Architecture Decision-Making & System Evolution