Guided Tutorials
Break a Monolith into Modules
Decompose a large, tangled codebase into focused modules with clear interfaces and dependency boundaries. This tutorial extracts one module at a time so the codebase stays working throughout the process.
What you'll learn
- How to analyze a codebase dependency graph to find natural module boundaries
- How to extract a tightly coupled cluster into a module with a clean public API
- How to update all consumers and enforce the module boundary
- How to iterate the extraction process across the remaining codebase
Before you start
- Autohand Code installed. Run
autohand --versionto confirm. See Your First Autohand Session if you need to install it. - A working test suite. At minimum, integration tests that exercise the main paths through the code. You will run these after every extraction.
- Git with a clean working tree. Each extracted module should be a separate commit so you can roll back individual extractions.
- A codebase you understand at a high level. You should know what the main responsibilities are (auth, billing, notifications, etc.).
Analyze dependencies
Before extracting anything, you need to understand the actual dependency structure. Gut feeling about what belongs together often does not match what the code actually does.
bash
autohand "Analyze the imports and function calls in src/. Build a dependency graph and show me which areas of the code are most tightly coupled. Identify clusters of code that only call each other and rarely get called from outside."The output will look something like this:
text
Dependency analysis for src/
Tightly coupled clusters:
Cluster A: Authentication (7 files, high cohesion)
src/auth/login.js calls: hashPassword, createSession, sendVerificationEmail
src/auth/session.js calls: validateToken, refreshSession
src/auth/verify.js calls: validateToken, markEmailVerified
src/utils/token.js called by: session.js, verify.js (auth only)
src/utils/hash.js called by: login.js (auth only)
src/email/verify.js called by: login.js (auth only)
External dependencies: bcrypt, jsonwebtoken, nodemailer
Boundary issues:
src/auth/login.js line 89: directly queries DB via src/db/users.js
src/auth/session.js line 45: reads config from src/config/app.js
Cluster B: Billing (5 files, medium cohesion)
...
High coupling (do not extract yet):
src/middleware/auth.js - called by 34 route files
src/db/index.js - imported by 28 filesThis output tells you which cluster is the safest first extraction. A cluster with high internal cohesion and a small, well-defined set of external dependencies is the right place to start.
Identify module boundaries
Based on the dependency analysis, ask Autohand to propose a clean boundary for the first module. The goal is to find a boundary that minimizes what needs to change in the rest of the codebase.
bash
autohand "Based on the dependency analysis, propose a clean public API for an auth module. The module should own everything in Cluster A. What functions should be part of the public API, what should stay internal, and what currently-external dependencies need to be abstracted?"A good proposed API looks like this:
typescript
// Proposed public API for auth module
export interface AuthModule {
// Login and session management
login(email: string, password: string): Promise<Session>;
logout(sessionId: string): Promise<void>;
refreshSession(sessionId: string): Promise<Session>;
// Token validation (used by middleware)
validateToken(token: string): Promise<TokenPayload | null>;
// Email verification
sendVerificationEmail(userId: string): Promise<void>;
verifyEmail(token: string): Promise<boolean>;
}
// Dependencies the module needs from the outside
export interface AuthDependencies {
db: { findUserByEmail: (email: string) => Promise<User | null> };
config: { jwtSecret: string; emailFrom: string };
mailer: { send: (opts: MailOptions) => Promise<void> };
}The AuthDependencies interface is important. It makes the module's external requirements explicit and injectable, rather than having the module reach out and grab them directly.
Extract the first module
With the boundary agreed on, ask Autohand to do the extraction. Be explicit about the target directory and the files to move.
bash
autohand "Extract the auth module. Move src/auth/, src/utils/token.js, src/utils/hash.js, and src/email/verify.js into a new src/modules/auth/ directory. Create an index.ts that exports only the public API. Do not change any behavior, only reorganize the files and update import paths."Autohand will:
- Move the files to the new location
- Update all relative imports within those files
- Create the
index.tswith only the public exports - Update all callers in the rest of the codebase to import from
src/modules/authinstead of the old paths
Tip: Do the file move and the API cleanup as two separate steps. Move first, update imports, run tests. Then clean up the public API. This makes each step easy to validate independently.
Define the public interface
After the files are moved, enforce the public API boundary. The module's index file should only export what external code should use.
bash
autohand "Audit src/modules/auth/index.ts. Make sure it only exports the functions in the agreed public API. Move any functions that are currently exported but should be internal to a src/modules/auth/internal/ subdirectory. Update any imports that break as a result."After this step, a good src/modules/auth/index.ts has no implementation details leaking through:
typescript
// src/modules/auth/index.ts
export { login, logout, refreshSession } from './session';
export { validateToken } from './token';
export { sendVerificationEmail, verifyEmail } from './verification';
export type { Session, TokenPayload, AuthDependencies } from './types';The internal files like hash.js and the email templates are not exported. Code outside the module cannot import them directly, which enforces the boundary.
Update consumers
The rest of the codebase was importing directly from the old paths. Autohand updated these during the extraction, but verify the update was complete.
bash
autohand "Search the entire codebase for any imports from the old auth paths: src/auth/, src/utils/token, src/utils/hash, src/email/verify. List any remaining references that still use the old paths."If any are found, update them:
bash
autohand "Update all remaining imports in src/middleware/ to import from src/modules/auth instead of the old paths."The middleware that validates tokens is usually the trickiest consumer because it runs on every request. Pay special attention to it:
typescript
// src/middleware/requireAuth.ts (updated)
import { validateToken } from '../modules/auth';
export async function requireAuth(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const payload = await validateToken(token);
if (!payload) return res.status(401).json({ error: 'Invalid token' });
req.user = payload;
next();
}Verify everything still works
Run the full test suite after the extraction is complete.
bash
npm testIf tests pass, commit the extracted module as a single clean commit:
bash
git add src/modules/auth/ src/middleware/ src/routes/
git commit -m "refactor: extract auth into src/modules/auth with clean public API"If tests fail, ask Autohand to help diagnose:
bash
autohand "These tests are failing after the auth module extraction. Read the test output and identify which imports or exports are missing or incorrect:
[paste test output here]"Extract the next module
Once the auth module is stable, repeat the process for the next cluster from your dependency analysis. Each extraction is easier than the last because you now have a working pattern and a cleaner codebase to work with.
bash
autohand "Now extract the billing cluster into src/modules/billing/. Use the same approach as the auth module: move files, create an index.ts with a clean public API, update all consumers. The auth module extraction from the previous step is a good reference for the expected structure."After two or three extractions, look at what is left in the old directories. Often a large portion of the monolith has been moved, and what remains is thin glue code or the application entry point itself.
bash
autohand "Summarize what remains in src/ outside of src/modules/. How much of it is still tangled, and what is the best order to extract the remaining pieces?"What you learned
- Analyzed a codebase dependency graph to identify tightly coupled clusters
- Extracted the auth cluster into a standalone module with an explicit public API
- Updated all consumers to import from the module and verified tests still pass
- Established a repeatable pattern for extracting additional modules from the monolith