Maintaining a clean, organized Emacs configuration becomes increasingly important as your setup grows. A modular approach separates concerns, simplifies troubleshooting, and makes your configuration more maintainable over time. In this article, I’ll share how I structure my Emacs configuration for maximum flexibility and sustainability.

intro

The Problem with Monolithic Configurations

Many Emacs users start with a single .emacs or init.el file. This works fine at first, but as your configuration grows to accommodate different programming languages, workflows, and packages, a single file becomes unwieldy. Common issues include:

  • Hard-to-find settings when you need to make changes
  • Difficult debugging when something breaks
  • Challenging to selectively enable/disable features
  • Poor organization making it hard to understand your own setup

A Modular Approach

Let’s explore a modular configuration structure based on my personal setup that addresses these issues.

Root Configuration File

The entry point for my configuration is _0x17de-emacs.el, which defines the base setup and loads other modules:

(setq initial-scratch-message nil
      ring-bell-function #'ignore)

(defvar _0x17de/load-path (file-name-directory (or load-file-name buffer-file-name))
  "Path of the _0x17de emacs config")

(defun _0x17de/load (paths)
  "Load files relative to this emacs config."
  (when (not (listp paths))
    (setq paths (list paths)))
  (dolist (path paths)
    (condition-case err
        (load (expand-file-name path _0x17de/load-path))
      (error (message "Failed to load _0x17de-config sub-library at %S: %S" path err)))))

The key here is the _0x17de/load function that loads modules with error handling, preventing a failure in one module from breaking your entire configuration.

Directory Structure

My configuration is organized into clear directories:

  • init/ - Core settings (startup, UI, packages)
  • langs/ - Language-specific configurations
  • utils/ - General utilities and enhancements
  • ext/ - External packages not available in package repositories

This structure makes it immediately clear where to find specific settings.

Core Initialization

The initialization sequence loads modules in logical groups:

(_0x17de/load
 '("./init/custom.el"
   "./init/speedup.el"
   "./init/package.el"
   "./init/encoding.el"
   "./init/gui.el"
   "./init/exwm.el"))

(_0x17de/load
 '("./utils/exec-path-from-shell"
   "./utils/minibuffer"
   "./utils/indention"
   ;; ... more utilities ... 
   ))

(_0x17de/load
 '("./langs/common"
   "./langs/ansible"
   "./langs/plantuml"
   ;; ... more languages ...
   ))

This approach lets you:

  1. Control the order of initialization
  2. Group related components
  3. Easily comment out modules you don’t want to load

Graceful Error Handling

A crucial aspect of this setup is the error handling in the _0x17de/load function. If a module fails to load, it displays a message but continues loading other modules:

(condition-case err
    (load (expand-file-name path _0x17de/load-path))
  (error (message "Failed to load _0x17de-config sub-library at %S: %S" path err)))

This prevents a single module failure from breaking your entire Emacs experience.

Specialized Module Examples

Let’s examine a few module types to see how they encapsulate specific functionality.

Language Support

Language-specific modules handle everything needed for a particular language. For example, my Go configuration:

(use-package go-mode
  :defer t
  :bind
  (:map go-mode-map
        ([tab] . 'company-indent-or-complete-common)
        ([f1] . lsp-describe-thing-at-point)
        ([f12] . lsp-find-definition)
        ("S-<f12>" . lsp-find-references))
  :hook
  (go-mode . (lambda ()
               (setq tab-width 2)
               (setq company-backends '(company-capf
                                        company-files))
               (setq gofmt-command "goimports")
               (add-hook 'before-save-hook 'gofmt-before-save)
               (go-guru-hl-identifier-mode)
               (lsp-deferred)
               ;; ... more settings ...
               )))

This module:

  • Loads only when editing Go files (:defer t)
  • Sets up keybindings specific to Go files
  • Configures language-specific tools and formatting
  • Integrates with LSP for auto-completions and advanced IDE features

Utility Modules

Utilities enhance the core editing experience regardless of the file type you’re editing. For example, my multiple-cursors configuration:

(defun _0x17de/add-multiple-cursors-to-non-empty-lines (start end)
  "Add a cursor in each non-empty line of selection"
  (interactive "r")
  ;; ... implementation ...
  )

(use-package multiple-cursors
  :init
  (global-unset-key (kbd "M-<down-mouse-1>"))
  :bind
  (("C-M-z C-c" . mc/edit-lines)
   ("C-M-z C-M-c" . _0x17de/add-multiple-cursors-to-non-empty-lines)
   ("C-M-z >" . mc/mark-next-like-this)
   ;; ... more bindings ...
   ))

This module encapsulates a complete feature set with custom functions and keybindings.

Feature Flags

For optional features, I use custom variables as feature flags:

(defcustom _0x17de/use-exwm nil
  "Non-nil means EXWM will be enabled.
When this option is enabled, EXWM will be loaded and configured
as the window manager for this session."
  :type 'boolean
  :group '_0x17de)

(when _0x17de/use-exwm
  ;; ... EXWM configuration ...
  )

This allows toggling features without commenting out code or editing multiple files.

Benefits of This Approach

This modular approach offers several advantages:

  1. Maintainability: Each module has a single responsibility
  2. Performance: Lazy loading modules when needed
  3. Portability: Easy to share modules between configurations
  4. Resilience: Failures in one module don’t break everything
  5. Clarity: Clear organization makes finding settings easier
  6. Flexibility: Enable/disable features without editing code

Leveraging Built-in Emacs Utilities

Emacs provides several powerful utilities that make modular configurations easier to implement and maintain.

use-package: Package Management Made Simple

While not built into Emacs core (but bundled with Emacs 29+), use-package is essential for modular configurations:

(use-package python
  :ensure nil  ; Don't try to install built-in packages
  :defer t     ; Only load when needed
  :bind (:map python-mode-map
          ([f12] . lsp-find-definition)
          ("S-<f12>" . lsp-find-references))
  :hook ((python-mode . (lambda ()
                          (company-mode t)
                          (flycheck-mode t)
                          (lsp-deferred))))
  :config     ; Executed after the package loads
  (setq python-indent-offset 4))

Key use-package keywords:

  • :ensure - Controls package installation
  • :defer - Enables lazy loading
  • :bind - Sets up keybindings (with automatic lazy loading)
  • :hook - Adds mode hooks (with automatic lazy loading)
  • :init - Code executed before loading
  • :config - Code executed after loading
  • :custom - Sets custom variables
  • :commands - Creates autoloaded commands (callable via M-x or keybindings)

This declarative approach keeps package configuration concentrated in one place rather than scattered throughout your init file.

defcustom: User-Configurable Variables

defcustom creates proper user options with documentation, custom types, and integration with Emacs’ customization interface:

(defcustom _0x17de/python-global-virtualenv-dir "~/.venv"
  "Default directory for Python virtual environments.
This is used by pyvenv to locate and activate virtual environments."
  :type 'directory
  :group '_0x17de
  :safe #'stringp)

Benefits of defcustom over regular variables:

  1. Type checking - The :type property validates values
  2. Documentation - Self-documents the option
  3. UI integration - Works with customize-* commands
  4. Safety - The :safe property helps with security
  5. Organization - Options can be grouped logically

define-minor-mode: Creating Toggleable Features

For modular features that can be enabled/disabled, define-minor-mode is perfect:

(define-minor-mode latex-compile-on-save-mode
  "Refresh the preview on save"
  :lighter " LTeXcos"
  :group latex-compile-on-save
  (if latex-compile-on-save-mode
      (add-hook 'after-save-hook 'latex-compile-on-save--compile nil t)
    (remove-hook 'after-save-hook 'latex-compile-on-save--compile t)))

This creates a toggleable mode with:

  • A proper minor mode that can be enabled/disabled with M-x
  • Automatic hook management when enabled/disabled
  • Integration with the mode line via :lighter

eval-after-load: Targeted Configuration

For smaller configurations without use-package, eval-after-load provides similar lazy-loading capabilities:

(eval-after-load 'python-mode
  '(progn
     (define-key python-mode-map (kbd "C-c C-r") 'python-shell-send-region)
     (setq python-indent-offset 4)))

This defers execution until the specified feature is loaded, improving startup times.

with-eval-after-load: Modern Alternative

A more modern, macro-based version of eval-after-load:

(with-eval-after-load 'org
  (setq org-hide-emphasis-markers t)
  (add-hook 'org-mode-hook #'visual-line-mode))

This is cleaner than eval-after-load because it doesn’t require quoting the body.

autoload: Manual Lazy Loading

For maximum control over lazy loading:

;;;###autoload
(defun my/open-config-file ()
  "Open the main configuration file."
  (interactive)
  (find-file (expand-file-name "_0x17de-emacs.el" _0x17de/load-path)))

The ;;;###autoload cookie tells Emacs to make the function available without loading the entire file.

Implementation Tips

If you want to build a similar configuration:

  1. Start small: Begin with a single file and add more as needed
  2. Use consistent naming: Establish a convention for module files
  3. Leverage use-package: For declarative, lazy-loaded package configuration
  4. Add error handling: Ensure graceful recovery from failures
  5. Document with defcustom: For user-facing configuration options
  6. Use minor modes: For toggleable features
  7. Embrace lazy loading: With autoload, eval-after-load, and with-eval-after-load

Conclusion

A modular Emacs configuration significantly improves maintainability and flexibility. By separating concerns into discrete modules with clear responsibilities, you can build a robust, personalized environment that evolves with your needs while remaining manageable.

This approach has served me well, allowing my configuration to grow from a few basic settings to a comprehensive development environment without becoming unwieldy. The time invested in proper organization pays dividends in the long term through improved stability and ease of modification.

Feel free to adapt this structure to your own needs or explore my complete configuration for more ideas and inspiration.