Loanable Funds & Crowding Out

The market for loanable funds

Think of all saving and borrowing in the economy as one market for a single good — loanable funds — whose “price” is the real interest rate \(r\).

  • Demand = investment \(I(r)\). Firms borrow to build capital; they borrow less when \(r\) is high, so demand slopes down.
  • Supply = national saving \(S\). It splits into private saving (households) and public saving (the government):

\[S = \underbrace{(Y - T - C)}_{\text{private}} + \underbrace{(T - G)}_{\text{public}} = Y - C - G\]

  • Equilibrium: \(r\) adjusts until saving equals investment, \(S = I(r)\).

The three “savings”

  • Private saving \(= Y - T - C\) — what households keep after taxes and consumption.
  • Public saving \(= T - G\) — the government’s balance (a deficit when \(G > T\)).
  • National saving \(= Y - C - G\) — the total pool that funds investment.

Comparative statics

  • Deficit up (raise \(G\), or cut \(T\)): national saving falls → the saving line shifts left → \(r\) rises and investment is squeezed. This is crowding out.
  • Investment demand up: the \(I(r)\) line shifts right → \(r\) rises. Whether investment actually rises depends on saving:
    • if saving is vertical (consumption doesn’t depend on \(r\)), total investment is unchanged — the higher \(r\) chokes off exactly the extra demand;
    • if saving slopes up (consumption responds to \(r\)), higher \(r\) pulls in more saving, so investment rises.

Try it below. Push the government into deficit and watch \(r\) climb and investment fall. Then flip on “consumption responds to \(r\)” and shift investment demand — see why investment moves only when saving slopes up.

#| standalone: true
#| viewerHeight: 560

library(shiny)

ui <- fluidPage(
  tags$head(tags$style(HTML("
    body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
    .stats-box { background:#f0f4f8; border-radius:6px; padding:12px 14px; margin-top:10px;
                 font-size:14px; line-height:1.85; }
    .stats-box b { color:#1f3b73; }
    .up { color:#b5462a; font-weight:bold; }
    .dn { color:#1c6b4a; font-weight:bold; }
  "))),
  sidebarLayout(
    sidebarPanel(
      width = 4,
      sliderInput("G",  "Government purchases  G:", min = 800, max = 2500, value = 1500, step = 50),
      sliderInput("T",  "Taxes  T:",                min = 800, max = 2500, value = 1500, step = 50),
      sliderInput("dI0","Investment-demand shift:", min = -400, max = 400, value = 0, step = 50),
      checkboxInput("rsens", "Consumption responds to r (saving slopes up)", value = FALSE),
      uiOutput("stats")
    ),
    mainPanel(
      width = 8,
      plotOutput("plot", height = "460px")
    )
  )
)

server <- function(input, output, session) {

  # fixed structural parameters
  Y <- 6000; C0 <- 600; mpc <- 0.6; b <- 100; I0 <- 1600

  eq <- function(G, T, dI0, cr) {
    rstar <- (I0 + dI0 - (Y - C0 - mpc*(Y - T) - G)) / (cr + b)
    Cstar <- C0 + mpc*(Y - T) - cr*rstar
    list(r = rstar,
         I = I0 + dI0 - b*rstar,
         Spriv = Y - T - Cstar,
         Spub  = T - G,
         Snat  = (Y - T - Cstar) + (T - G))
  }

  vals <- reactive({
    cr <- if (isTRUE(input$rsens)) 50 else 0
    cur  <- eq(input$G, input$T, input$dI0, cr)
    base <- eq(1500, 1500, 0, cr)     # reference: balanced budget, no shift
    Sr <- function(r) Y - (C0 + mpc*(Y - input$T) - cr*r) - input$G
    Ir <- function(r) I0 + input$dI0 - b*r
    list(cur = cur, base = base, Sr = Sr, Ir = Ir, cr = cr)
  })

  output$plot <- renderPlot({
    v <- vals(); rg <- seq(0, 16, length.out = 200)
    par(mar = c(4.2, 4.4, 1.2, 1))
    plot(NA, xlim = c(0, 2500), ylim = c(0, 16),
         xlab = "Loanable funds  (S, I)", ylab = "Real interest rate  r  (%)",
         las = 1, bty = "l", cex.lab = 1.15)
    points(v$base$I, v$base$r, pch = 1, col = "#9aa4b2", cex = 1.4, lwd = 2)
    lines(v$Sr(rg), rg, col = "#1f3b73", lwd = 3)
    lines(v$Ir(rg), rg, col = "#b5462a", lwd = 3)
    points(v$cur$I, v$cur$r, pch = 19, col = "#1c6b4a", cex = 1.7)
    segments(0, v$cur$r, v$cur$I, v$cur$r, lty = 3, col = "#5a6472")
    segments(v$cur$I, 0, v$cur$I, v$cur$r, lty = 3, col = "#5a6472")
    legend("topright",
           c("Saving  S(r)", "Investment  I(r)", "Equilibrium", "Baseline (G=T, no shift)"),
           col = c("#1f3b73", "#b5462a", "#1c6b4a", "#9aa4b2"),
           lwd = c(3, 3, NA, NA), pch = c(NA, NA, 19, 1), bty = "n", cex = 1.0)
  })

  output$stats <- renderUI({
    v <- vals(); c <- v$cur
    deficit <- input$G - input$T
    dI <- c$I - v$base$I
    budget <- if (deficit > 0) sprintf("deficit of %.0f", deficit)
              else if (deficit < 0) sprintf("surplus of %.0f", -deficit)
              else "balanced budget"
    crowd <- if (abs(dI) < 1) "investment is unchanged vs. baseline"
             else if (dI < 0) sprintf("investment is <span class='up'>crowded out by %.0f</span> vs. baseline", -dI)
             else sprintf("investment is <span class='dn'>higher by %.0f</span> vs. baseline", dI)
    HTML(sprintf(
      "<div class='stats-box'>
         Budget: <b>%s</b><br>
         Private saving = <b>%.0f</b> &nbsp; Public saving = <b>%.0f</b><br>
         National saving = <b>%.0f</b><br>
         Equilibrium: <b>r* = %.2f%%</b>, &nbsp; <b>I* = %.0f</b><br>
         %s
       </div>",
      budget, c$Spriv, c$Spub, c$Snat, c$r, c$I, crowd))
  })
}

shinyApp(ui, server)

What to notice

  • Run a deficit — raise \(G\) above \(T\). The saving line slides left, \(r\) climbs, and the equilibrium dot moves up the investment curve: less investment. That’s crowding out, live.
  • Shift investment demand with saving vertical (box unchecked): \(r\) rises but \(I^*\) stays put — the extra demand is exactly offset by the higher rate.
  • Now check “consumption responds to \(r\)” and shift investment demand again: the saving line tilts, so higher \(r\) coaxes out more saving and investment actually rises. This is the difference between the two exam cases.