Adding analytics to your monolith doesn’t mean ripping it apart or introducing a complex microservice architecture.

Let’s see it in action. Imagine a simple e-commerce monolith. We want to track product views and add-to-cart events.

Here’s a snippet of a hypothetical Ruby on Rails controller:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    # Existing logic to render product details...

    # --- Analytics Integration ---
    AnalyticsEvent.create!(
      event_type: 'product_view',
      user_id: current_user.id, # Assuming current_user is available
      properties: {
        product_id: @product.id,
        product_name: @product.name,
        timestamp: Time.current
      }
    )
    # ---------------------------
  end

  def add_to_cart
    @product = Product.find(params[:id])
    # Existing logic to add product to cart...

    # --- Analytics Integration ---
    AnalyticsEvent.create!(
      event_type: 'add_to_cart',
      user_id: current_user.id,
      properties: {
        product_id: @product.id,
        product_name: @product.name,
        quantity: params[:quantity].to_i || 1,
        timestamp: Time.current
      }
    )
    # ---------------------------
  end
end

And the corresponding AnalyticsEvent model and database table:

# app/models/analytics_event.rb
class AnalyticsEvent < ApplicationRecord
  # No complex validations needed here for raw event logging
end

# Migration to create the table (example for PostgreSQL)
class CreateAnalyticsEvents < ActiveRecord::Migration[7.0]
  def change
    create_table :analytics_events do |t|
      t.string :event_type, null: false
      t.integer :user_id # Could be null for anonymous users
      t.jsonb :properties, null: false, default: {}
      t.timestamps
    end
    add_index :analytics_events, :event_type
    add_index :analytics_events, :user_id
    add_index :analytics_events, :properties, using: 'gin' # For efficient JSONB querying
  end
end

This setup allows you to capture granular user interactions directly within your existing application code. The event_type categorizes the action, user_id links it to a user (if logged in), and properties provides rich, context-specific details. The jsonb data type in PostgreSQL is crucial here, allowing for flexible and performant querying of the event data.

The core problem this solves is the need for detailed, event-driven insights into user behavior without the overhead of a separate analytics service. Instead of guessing what users are doing, you’re recording it. This data can then power dashboards, trigger personalized experiences, or inform product decisions. Internally, it’s a simple write-heavy operation. The monolith handles the request, logs the event, and proceeds with its primary task. The analytics logging is a side-effect, a secondary concern handled efficiently by the database.

You control the granularity and richness of your analytics by defining the event_type strings and the structure of the properties hash. For instance, you might add an order_complete event with properties like order_id, total_amount, and items_purchased.

A common pitfall is over-indexing on the properties column for filtering. While gin indexes on jsonb are powerful, complex queries that involve deeply nested or highly selective filters within properties can still become performance bottlenecks. It’s often more efficient to extract frequently filtered properties into dedicated, indexed columns if a pattern emerges. For example, if you always filter add_to_cart events by product_id, consider a separate product_id column on the analytics_events table.

This approach allows you to build a robust analytics foundation directly within your monolith, providing immediate insights into user journeys and application usage. The next logical step is to explore how to process and visualize this raw event data.

Want structured learning?

Take the full Monolith course →