You run a modular monolith and want to add serverless functions at the edges. Which workloads belong in the serverless edge, which must stay in the monolith, and what integration hazards should you plan for?
answer
- edge = triggered, spiky, self-contained, stateless
- core = shared invariants, one transaction, low latency
- never let functions touch the monolith's tables
- cold starts, connection pools, at-least-once, time limits
- propagate trace ids; version contracts, both deploy separately
basics
~20 sPut bursty, event-triggered, self-contained work in functions: media processing, webhook intake, scheduled jobs, notifications. Keep core transactional business rules in the monolith. Functions should call the monolith's published API or consume its events, never read or write its database directly.
solid answer
~50 sServerless suits work that is stateless, short, independently scalable and triggered by an event: image or document processing, webhook receivers, scheduled reports, fan-out notifications, glue between third-party systems, and edge concerns such as redirects or token checks at the CDN. The monolith keeps anything that touches shared invariants and multi-entity transactions, anything chatty against the domain model, and anything needing low, predictable latency. The decisive rule is data ownership: functions must integrate through the monolith's public API or its events, never against its schema, otherwise every function becomes an undeclared dependency on internal tables and the module boundaries you paid for evaporate. Plan for cold starts on user-facing paths, connection-pool exhaustion when many function instances open database connections, at-least-once triggers demanding idempotency, execution time and payload limits, duplicated domain logic drifting from the monolith, split deployment and versioning, secret and IAM sprawl, weaker local development fidelity, and distributed tracing across two very different runtimes.
go deeper
Name examples that belong in functions - image processing, scheduled jobs, webhook receivers - and say the core business logic stays in the monolith.
Give the selection criteria (triggered, bursty, stateless, self-contained) and the main hazards: cold starts, retries needing idempotency, execution limits.
Lead with the data-ownership rule (API or events, never the schema), discuss connection-pool exhaustion, contract versioning across independently deployed pieces, and end-to-end tracing.
Frame it as scope control and cost: a paved-road template for functions (IAM, logging, tracing, infrastructure-as-code), a rule that the edge holds no durable business state, backward-compatible event contracts, and criteria for when a growing edge should instead become a proper service or return to the core.
## The two pieces A **modular monolith** is one deployable application whose internals are split into modules with enforced boundaries (public interfaces per module, no reaching into another module's internals or tables), typically one transactional database. It gives you in-process calls, ACID transactions across modules, one deployment and one debugging story. **Serverless / functions-as-a-service** means short-lived functions the platform runs on demand, scaling instances with load and billing per invocation and duration. There is no server you manage, no long-lived local state, and hard limits on execution time, memory and payload size. Combining them is the most common hybrid in mid-sized systems: keep the transactional core intact, push spiky or peripheral work outward. ## What belongs in the serverless edge - **Bursty, embarrassingly parallel work**: thumbnailing, transcoding, virus scanning, PDF or export generation. Load is spiky and idle cost should be zero. - **Event-triggered glue**: react to an object landing in a bucket, a queue message, a third-party webhook, a database change stream. - **Scheduled jobs**: nightly reconciliation, cleanup, digest emails - work you do not want holding capacity or competing with request traffic inside the monolith. - **Ingress buffers**: webhook receivers that validate a signature, enqueue and return quickly, protecting the core from third-party burst and retry storms. - **Edge concerns**: cache rules, redirects, A/B routing, cheap token validation executed near the user. - **Experiments and low-traffic side features** where zero idle cost dominates. ## What must stay in the monolith - **Core transactional business rules** that must commit atomically across several entities. Splitting them out converts a local transaction into a distributed one and forces sagas and compensations for no benefit. - **Anything chatty against the domain model** - logic needing many reads and writes per operation is slow and expensive at the edge. - **Latency-sensitive interactive paths** where a cold start adds hundreds of milliseconds or more. - **Logic that would be duplicated**: if a function needs pricing or eligibility rules, it should ask the monolith rather than reimplement them, or the two copies will drift and produce inconsistent decisions. ## The one non-negotiable integration rule **Functions integrate through the monolith's published API or its events, never directly against its database schema.** Direct table access from functions is the single failure that ruins this hybrid: it creates undeclared consumers of internal tables, so the monolith can no longer refactor its own schema; the module boundaries it enforces internally are bypassed from outside; invariants enforced in domain code are silently skipped by raw writes; and nothing in the monolith's build can detect the coupling. If a function truly needs bulk read access, give it a read replica of an explicitly published, versioned view - not the internal tables. ## Hazards to plan for 1. **Cold starts** - the first invocation on a new instance pays initialisation. Fine for asynchronous jobs, harmful on synchronous user paths. Mitigate with provisioned or warm concurrency, small runtimes and lean dependencies, or by keeping that path in the monolith. 2. **Connection exhaustion** - a monolith uses a bounded pool; a thousand concurrent function instances each opening a relational connection will exhaust the database. Use a connection proxy or pooler, reserved concurrency limits, or route data access through the monolith's API. 3. **At-least-once triggers** - most event sources may invoke a function more than once. Handlers must be idempotent (dedupe key, conditional write) and need a dead-letter queue with an alert and a replay path. 4. **Execution limits** - maximum duration, memory and payload size. Long jobs must be chunked or moved to a container or batch runtime. 5. **Logic duplication and drift** - shared rules copied into functions diverge; prefer calling back into the monolith or extracting a genuinely shared, versioned library with a deprecation policy. 6. **Split deployment and versioning** - the monolith and its functions now release independently, so an event or API contract change must be backward compatible for at least one release, with expand-then-contract migrations. 7. **Operational sprawl** - separate IAM roles, secrets, logging destinations, alerting and infrastructure-as-code for every function; without a template this becomes dozens of snowflakes. 8. **Observability gap** - propagate correlation and trace ids from the monolith through the event payload into the function, or you lose end-to-end tracing exactly where failures are hardest to see. 9. **Local development fidelity** - developers can run the monolith; emulating the trigger topology is harder, so invest in local emulators or a shared development environment. 10. **Vendor coupling** - trigger models, event schemas and identity are provider-specific. Keep the function body a thin adapter around portable logic if exit cost matters. ## How to decide, in one line If the work is triggered rather than requested, tolerant of eventual consistency, self-contained, and spiky in volume, it belongs at the edge. If it enforces a shared invariant inside one transaction, it belongs in the core.
- Why is direct database access from an edge function to the monolith's tables considered the worst failure mode of this hybrid?It makes the function an undeclared consumer of internal schema, so the monolith can no longer refactor its own tables safely; it bypasses invariants and validation enforced in domain code; it defeats the module boundaries the monolith enforces internally; and no build-time check in either codebase can detect the coupling. The result is a distributed monolith with none of the discipline that would make it survivable.
- A user-facing endpoint moved to a function now shows a slow first request after idle periods. What are your options?Reduce initialisation cost (smaller runtime, fewer dependencies, lazy client creation, snapshot or ahead-of-time start features), keep instances warm with provisioned concurrency or a scheduled ping, move the path back into the always-on monolith, or make it asynchronous so the latency is not user-visible. Which one is right depends on whether the tail latency is a real requirement or just untuned.
- How do you keep business rules from being duplicated between the monolith and its edge functions?Keep the rules in the core and have the function call the monolith's API or receive the decision in the triggering event payload, so the function stays a thin adapter. If the logic genuinely must run at the edge, extract it into a versioned shared library with an explicit compatibility and deprecation policy, and add contract tests. A copy-paste with no owner will silently diverge.
A restaurant keeps every dish that needs the shared stock pot and precise timing on the main line. Deliveries, dishwashing and the weekend outdoor barbecue move to separate stations that scale up only when needed - but no station is allowed to walk in and take things straight from the pot.
saying these in an interview costs you the question
- Letting functions query or update the monolith's internal tables
- Assuming serverless is always cheaper without accounting for chatty calls, data transfer and high-volume steady workloads
- Ignoring cold starts on synchronous user-facing paths
- Opening a relational connection per function instance with no pooler or concurrency cap
- Copying business rules into functions instead of calling the core, then letting the copies drift
- Treating an at-least-once trigger as exactly-once, with no idempotency and no dead-letter queue
- Skipping trace-context propagation, leaving the edge invisible in end-to-end debugging