Chapter 2 Environment Setup & Shared Infrastructure

Before diving into specific projects, we need to establish the shared infrastructure that every production system requires: database connections, logging, configuration management, and utility functions. This chapter builds the foundation.

2.1 Package Management with renv

Production systems demand reproducibility. The renv package creates isolated, reproducible environments for R projects.

# Initialize renv in your project
renv::init()

# Install packages — they are recorded in renv.lock
install.packages("tidymodels")

# Snapshot the environment
renv::snapshot()

# On a new machine, restore the exact environment
renv::restore()

Tip: Commit renv.lock to version control, but add renv/library/ to .gitignore.

2.2 Database Connection Management

2.2.1 Secure Credential Handling

Never hardcode credentials. Use environment variables or secret management systems.

# .Renviron file (add to .gitignore!)
DB_USER=analytics_user
DB_PASS=your_secure_password
DB_HOST=prod-db.company.com
DB_PORT=5432
DB_NAME=analytics
# In your R code
library(DBI)
library(RPostgres)

con <- dbConnect(
  RPostgres::Postgres(),
  dbname = Sys.getenv("DB_NAME"),
  host = Sys.getenv("DB_HOST"),
  port = as.integer(Sys.getenv("DB_PORT")),
  user = Sys.getenv("DB_USER"),
  password = Sys.getenv("DB_PASS")
)

2.2.2 Connection Pooling

For applications serving multiple concurrent users (Shiny apps, Plumber APIs), connection pooling prevents exhausting database resources.

library(pool)

db_pool <- dbPool(
  drv = RPostgres::Postgres(),
  dbname = Sys.getenv("DB_NAME"),
  host = Sys.getenv("DB_HOST"),
  port = as.integer(Sys.getenv("DB_PORT")),
  user = Sys.getenv("DB_USER"),
  password = Sys.getenv("DB_PASS"),
  minSize = 2,      # Minimum connections
  maxSize = 10,     # Maximum connections
  idleTimeout = 300 # Close idle connections after 5 minutes
)

# Use pool exactly like a regular connection
pool::poolReturn(db_pool)

2.2.3 Lazy Evaluation with dbplyr

The dbplyr package translates dplyr verbs into SQL, allowing you to work with database tables as if they were in-memory data frames.

library(dbplyr)

# This does NOT load data into R — it builds a SQL query
enterprise_customers <- tbl(con, "customers") %>%
  filter(segment == "enterprise", mrr > 10000) %>%
  select(customer_id, company_name, mrr, created_at)

# View the generated SQL
enterprise_customers %>% show_query()

# Only collect() brings data into R
results <- enterprise_customers %>% collect()

Important: Always filter and aggregate before collect(). Bringing unfiltered data into R is the most common cause of memory issues in production.

2.3 Logging Infrastructure

Structured logging is essential for debugging production issues.

library(logger)

# Configure logging
log_threshold(INFO)
log_layout(layout_glue_colors)

# Usage throughout your code
log_info("Starting feature engineering pipeline")
log_warn("Missing values detected in {n_missing} rows")
log_error("Model training failed: {e$message}")

# For production, log to files or external systems
log_appender(appender_file("/var/log/r-app.log"))

2.4 Safe Execution Patterns

Production code must handle failures gracefully.

safe_execute <- function(expr, fallback = NULL, context = "operation") {
  tryCatch(
    expr,
    error = function(e) {
      log_error("[{context}] Failed: {e$message}")
      fallback
    },
    warning = function(w) {
      log_warn("[{context}] Warning: {w$message}")
      invokeRestart("muffleWarning")
    }
  )
}

# Usage
model <- safe_execute(
  xgboost::xgb.train(params, dtrain, nrounds = 100),
  fallback = NULL,
  context = "model_training"
)

2.5 Health Checks

Every production service should expose a health endpoint.

#* @get /health
function() {
  list(
    status = "healthy",
    timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
    version = "1.0.0",
    db_connected = DBI::dbIsValid(con)
  )
}

2.6 Configuration Management

Centralize configuration to avoid magic numbers scattered through code.

# config.yml
# default:
#   db:
#     host: "localhost"
#     port: 5432
#   model:
#     churn_threshold: 0.7
#     retrain_frequency: "weekly"
# 
# production:
#   db:
#     host: "prod-db.company.com"
library(config)

cfg <- config::get()

# Access configuration
db_host <- cfg$db$host
churn_threshold <- cfg$model$churn_threshold

2.7 Shared Utility Functions

Create a utils.R file with functions used across projects.

# R/utils.R

#' Format currency values
format_currency <- function(x, currency = "$") {
  scales::dollar(x, prefix = currency)
}

#' Calculate date ranges for reporting
get_reporting_period <- function(period = "last_30_days") {
  end_date <- Sys.Date()
  start_date <- switch(period,
    "last_7_days" = end_date - 7,
    "last_30_days" = end_date - 30,
    "last_quarter" = end_date - 90,
    "last_year" = end_date - 365,
    end_date - 30
  )
  list(start = start_date, end = end_date)
}

#' Save model with metadata
save_model_artifact <- function(model, path, metadata = list()) {
  artifact <- list(
    model = model,
    metadata = c(
      metadata,
      list(
        saved_at = Sys.time(),
        r_version = R.version.string,
        session_info = utils::sessionInfo()
      )
    )
  )
  readr::write_rds(artifact, path, compress = "xz")
}

2.8 Testing with testthat

Production code requires automated tests.

library(testthat)

# tests/testthat/test-utils.R
test_that("format_currency handles basic cases", {
  expect_equal(format_currency(1000), "$1,000")
  expect_equal(format_currency(0), "$0")
  expect_true(is.na(format_currency(NA)))
})

test_that("get_reporting_period returns valid dates", {
  period <- get_reporting_period("last_30_days")
  expect_equal(period$end - period$start, 30)
})

2.9 Exercises

  1. Set up renv for a new project and install all packages from this chapter.
  2. Write a connection pool wrapper that automatically retries failed connections up to 3 times with exponential backoff.
  3. Create a logging configuration that writes INFO and above to the console, but only WARN and above to a file.
  4. Write unit tests for the safe_execute() function, verifying it returns the fallback on error and logs appropriately.

With our infrastructure in place, we’re ready to build our first production project: a customer churn prediction system with automated retention workflows.