In a monolithic application, all the business logic, UI, and data-access code are compiled and deployed together as one running program. What does 'shared process and memory' mean for how components inside that application call each other, compared to components that live in separate services?
answer
- one process, one heap
- in-process call = no serialization
- shared transaction across modules
- no network boundary = no enforced boundary
- shared fate on crash
basics
~10 sInside a monolith, all the code runs in one program, so different parts call each other directly through normal function calls, instantly and without any network involved.
solid answer
~40 sA monolith compiles and runs as a single OS process, so every module shares the same address space. A call from the checkout module to the pricing module is a normal in-language method call resolved by the compiler/runtime — no serialization, no network round-trip, typically nanoseconds to microseconds. This lets you share one database transaction across modules, get one unified stack trace on failure, and let the compiler catch cross-module type mistakes. The cost: nothing stops one module from reaching into another's internal state unless the team enforces boundaries with visibility modifiers and review discipline. And because it's one process, a fatal error in any module — an unhandled exception, an OOM, a thread pool exhausted by a hung call — can take the entire application down, not just the failing feature.
go deeper
Should be able to say that a monolith runs as one program so its parts call each other directly, and contrast that loosely with services talking over a network.
Should articulate concretely that in-process calls skip serialization and network I/O, and name at least one concrete benefit (shared transactions) and one concrete risk (shared failure/crash blast radius).
Should connect shared memory to the absence of an enforced boundary, explain how that leads to coupling over time without discipline, and describe a real failure mode like thread-pool starvation or OOM taking down unrelated features.
Should discuss how to get boundary enforcement back through tooling (ArchUnit-style checks, module visibility) without giving up the in-process performance and transactional benefits, i.e., the modular-monolith trade-off space.
## One artifact, one process A monolithic architecture packages an application's entire codebase — UI, business logic, data access — into a single deployable artifact (one JAR, WAR, or binary) that runs as a single operating-system process at runtime. Everything that artifact contains shares one memory space (heap and stack) and one instance of the language runtime. That single fact drives almost everything distinctive about monoliths, both good and bad. ## How a call between modules is resolved Mechanically, when one module's code invokes another module's function inside a monolith, the language runtime resolves it as an ordinary **in-process call**: - a jump or virtual-dispatch instruction; - arguments passed by reference or value on the stack or heap; - a return address pushed for when the callee finishes. Both caller and callee already agree on memory layout, type representation, and calling convention because they were compiled together. ## Contrast with a call across a network Contrast that with a call between two independently deployed services. The caller must: 1. serialize its arguments into a wire format (JSON, protobuf); 2. open or reuse a socket; 3. transmit bytes across a network stack; 4. have the callee deserialize and validate them; 5. reverse the whole process for the response — plus handle the new failure modes a network introduces (timeouts, partial failures, retries, out-of-order delivery). The monolith simply doesn't have that layer: a call either succeeds by returning normally or fails by throwing an exception that propagates up the same call stack. ## Why the model exists at all This shared-memory model exists because, historically, it is simply the default way software gets built: one team writes one codebase, a compiler or interpreter turns it into one artifact, and that artifact runs as one process on one or more identical machines. It sidesteps an entire category of distributed-systems concerns — service discovery, network partitions, eventual consistency, distributed tracing — that only appear once you split an application across process or network boundaries. For a large class of applications, especially early in a product's life, this is the right trade: you get the benefits of a distributed system (partition tolerance, independent scaling, polyglot tech per service) only when you actually need them, and until then those benefits are pure overhead. ## The upside of shared memory The upside of shared memory is concrete **productivity**: - A single `@Transactional` block (or equivalent) can span multiple 'modules' and commit atomically against one database connection, because there's truly one transaction manager in one process talking to one connection pool. - **Debugging** is a single stack trace instead of a distributed trace stitched across service logs. - **Refactoring** across a module boundary is a compiler-checked, IDE-assisted operation — rename a method and every caller updates — rather than a coordinated, versioned change across independently deployed services. - **Local development** is a single process to run and a single debugger to attach. ## The downside is the same coupling The downside is the same coupling working against you. Shared memory means there is no structural barrier stopping the billing module from directly instantiating and mutating a class that 'belongs' to the inventory module; only code review, package-private visibility, or architectural discipline (e.g., ArchUnit-style rules) enforces a boundary that, in a real distributed system, would be enforced by the network itself — you literally cannot call a private method on a service you can't reach. Over time, without active discipline, cross-module references accumulate into a 'big ball of mud' where nobody can change one part without understanding the whole. And because everything shares one process, **failure isolation collapses**: - A memory leak in a rarely used reporting feature raises heap pressure until the JVM triggers an `OutOfMemoryError` that kills checkout and every other in-flight request too. - A single unhandled exception on a background thread can, in some runtimes, crash the entire process. - A synchronous call to a slow third-party API from one module can exhaust a shared thread pool and starve requests belonging to completely unrelated modules — a classic 'noisy neighbor' failure that shows up in production as unrelated features going down together with no obvious common cause until someone traces it back to shared process resources. ## A concrete example A concrete example: a Spring Boot application with `order`, `inventory`, and `billing` packages all compiled into one `app.jar`. `OrderController` calls `inventoryService.reserve(orderId)` as a plain Java method call, and both participate in the same `@Transactional` method backed by one `DataSource`, so an order write and an inventory decrement commit or roll back together with zero extra coordination code. That same guarantee across independently deployed order and inventory services would require a saga, an outbox pattern, or a distributed transaction coordinator — meaningfully more code and operational complexity to get the same atomicity the monolith gets for free from shared memory.
- If a monolith's checkout module calls the inventory module and inventory throws an unhandled runtime exception, what happens to the checkout request?The exception propagates up the same call stack that invoked it, because both modules run in the same thread within the same process. Unless checkout wraps the call in a try/catch, the exception bubbles all the way up and the checkout request itself fails — there's no process boundary to contain the failure to 'just inventory'. If the exception is severe enough (stack overflow, OOM) it can take down the whole JVM, failing every other in-flight request too.
- How would a team keep the same in-process convenience of a monolith while still preventing modules from reaching into each other's internals?By enforcing boundaries in code rather than relying on the network: package-private/internal visibility so only a module's public API is reachable from outside, and automated architecture tests (e.g., ArchUnit, Spring Modulith's module verification) that fail the build if a forbidden cross-module reference appears. This is essentially the 'modular monolith' pattern — same single process and shared memory, but with compiler- and CI-enforced module boundaries standing in for what a network boundary would otherwise force.
- Does shared memory make a monolith automatically faster at runtime than an equivalent set of microservices?For any given cross-module interaction, yes — an in-process call is orders of magnitude faster than a network call to another service. But that advantage only matters for interactions that would otherwise cross a service boundary; it says nothing about the monolith's overall throughput or scalability, which is bounded by scaling the whole process rather than scaling just the hot path.
It's like everyone on a team working in the same open-plan room versus working in separate offices connected only by phone: in the open room you can lean over and hand someone a document instantly, but a fire in that room affects everyone, and nothing stops you from grabbing something off a coworker's desk without asking.
saying these in an interview costs you the question
- Describes a monolith as 'just one big file' rather than one deployable process with shared memory
- Claims modules in a monolith are automatically isolated from each other's failures
- Doesn't know that a crash in one part of a monolith can take down unrelated functionality
- Thinks in-process calls require the same serialization overhead as network calls
- Can't explain why a shared transaction across modules is easy in a monolith but hard across services