← Back to list

majestic-monolith
by jsmithdenverdev
OpenCode configuration
⭐ 0🍴 0📅 Jan 21, 2026
SKILL.md
name: majestic-monolith description: Majestic monolith architecture pattern for modular, single-deployment applications license: MIT compatibility: opencode metadata: category: architecture pattern: monolith deployment: single
What I do
I provide guidance on building majestic monolith applications - single deployments with strong modular organization.
When to use me
Load this skill when:
- Building a single-team application
- Need for ACID transactions across features
- Want simpler deployment and operations
- Have a shared domain model
- Team size < 20 developers
- Moderate scaling requirements
Principles
1. Modular Organization
Organize code into modules with clear boundaries, even though it's a single deployment.
src/
├── modules/
│ ├── users/
│ │ ├── domain/ # Business logic
│ │ ├── application/ # Use cases
│ │ ├── infrastructure/ # DB, external APIs
│ │ └── presentation/ # API controllers
│ ├── products/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── infrastructure/
│ │ └── presentation/
│ └── orders/
│ ├── domain/
│ ├── application/
│ ├── infrastructure/
│ └── presentation/
├── shared/ # Shared utilities
│ ├── database/
│ ├── auth/
│ └── logging/
└── app.ts # Application entry
2. Clear Module Boundaries
Modules should communicate through well-defined interfaces.
// ❌ Bad: Direct coupling
import { UserRepository } from '../users/infrastructure/UserRepository';
class OrderService {
constructor() {
this.userRepo = new UserRepository(); // Tight coupling!
}
}
// ✅ Good: Interface-based dependency
interface UserService {
findById(id: string): Promise<User | null>;
}
class OrderService {
constructor(private readonly userService: UserService) {}
async createOrder(userId: string, items: OrderItem[]) {
const user = await this.userService.findById(userId);
if (!user) throw new Error('User not found');
// Create order
}
}
3. Shared Database with Module Schemas
Use a single database but organize tables by module.
-- Users module schema
CREATE SCHEMA users;
CREATE TABLE users.users (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Products module schema
CREATE SCHEMA products;
CREATE TABLE products.products (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL
);
-- Orders module schema
CREATE SCHEMA orders;
CREATE TABLE orders.orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL, -- Foreign key to users.users
created_at TIMESTAMP DEFAULT NOW()
);
4. Dependency Direction
Dependencies should flow inward: Presentation → Application → Domain
// Domain layer (no dependencies)
export class Order {
constructor(
public readonly id: string,
public readonly userId: string,
public readonly items: OrderItem[],
public readonly status: OrderStatus
) {}
// Business logic
canCancel(): boolean {
return this.status === 'pending' || this.status === 'confirmed';
}
}
// Application layer (depends on domain)
export class CreateOrderUseCase {
constructor(
private readonly orderRepo: OrderRepository,
private readonly userService: UserService
) {}
async execute(input: CreateOrderInput): Promise<Order> {
// Validate user exists
const user = await this.userService.findById(input.userId);
if (!user) throw new Error('User not found');
// Create and save order
const order = new Order(/* ... */);
await this.orderRepo.save(order);
return order;
}
}
// Infrastructure layer (depends on application/domain)
export class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
await db.query(
'INSERT INTO orders.orders (id, user_id, status) VALUES ($1, $2, $3)',
[order.id, order.userId, order.status]
);
}
}
// Presentation layer (depends on application)
export class OrderController {
constructor(private readonly createOrder: CreateOrderUseCase) {}
async create(req: Request, res: Response) {
const order = await this.createOrder.execute(req.body);
res.status(201).json(order);
}
}
Advantages
- Simplicity: Single codebase, single deployment
- ACID Transactions: Strong consistency across modules
- Easier Debugging: Centralized logs, single process
- Lower Latency: In-process communication
- Simpler Operations: One service to deploy and monitor
- Cost Effective: Lower infrastructure costs
Challenges
- Single Point of Failure: Entire app goes down if one module fails
- Scaling Limits: Must scale entire application
- Deployment Coordination: All modules deploy together
- Module Coupling Risk: Requires discipline to maintain boundaries
Migration Path
When you outgrow the monolith:
- Identify module boundaries: Already modular? Good!
- Extract module to service: Start with least coupled module
- Introduce API gateway: Route requests to appropriate service
- Migrate data: Move module's tables to separate database
- Repeat: Gradually extract more modules
Tools & Patterns
Module Registration
// modules/index.ts
export function registerModules(app: Express) {
// Each module registers its routes
registerUserModule(app);
registerProductModule(app);
registerOrderModule(app);
}
// modules/users/index.ts
export function registerUserModule(app: Express) {
const router = Router();
// Dependency injection
const userRepo = new PostgresUserRepository();
const createUser = new CreateUserUseCase(userRepo);
const controller = new UserController(createUser);
router.post('/users', (req, res) => controller.create(req, res));
app.use('/api', router);
}
Dependency Injection Container
// container.ts
export class Container {
private services = new Map<string, any>();
register<T>(key: string, factory: () => T): void {
this.services.set(key, factory);
}
resolve<T>(key: string): T {
const factory = this.services.get(key);
if (!factory) throw new Error(`Service ${key} not found`);
return factory();
}
}
// setup.ts
const container = new Container();
// Register services
container.register('userRepository', () => new PostgresUserRepository());
container.register('createUser', () =>
new CreateUserUseCase(container.resolve('userRepository'))
);
// Use in controllers
const createUser = container.resolve<CreateUserUseCase>('createUser');
Event Bus for Module Communication
// Decouple modules with events
interface DomainEvent {
type: string;
payload: any;
timestamp: Date;
}
class EventBus {
private handlers = new Map<string, Array<(event: DomainEvent) => void>>();
subscribe(eventType: string, handler: (event: DomainEvent) => void): void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, []);
}
this.handlers.get(eventType)!.push(handler);
}
publish(event: DomainEvent): void {
const handlers = this.handlers.get(event.type) || [];
handlers.forEach(handler => handler(event));
}
}
// Usage
eventBus.subscribe('user.created', async (event) => {
// Send welcome email
await emailService.sendWelcome(event.payload.email);
});
// In user module
await userRepo.save(user);
eventBus.publish({
type: 'user.created',
payload: { userId: user.id, email: user.email },
timestamp: new Date(),
});
Best Practices
- Keep modules independent: Minimize inter-module dependencies
- Use interfaces: Define clear contracts between modules
- Avoid shared state: Each module owns its data
- Test modules in isolation: Unit tests shouldn't cross module boundaries
- Document module APIs: Clear public interface for each module
- Monitor module performance: Track which modules are slowest
References
- "The Majestic Monolith" by David Heinemeier Hansson
- "Modular Monolith" architectural pattern
- Clean Architecture by Robert C. Martin
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon