How should a codebase be organized? Splitting by technical responsibility (Controller, Service, Repository) is horizontal slicing. Splitting by feature or domain (User, Order, Payment) is vertical slicing. The organizing principle determines the scope of changes, inter-team dependencies, and deployment boundaries.

Horizontal Slicing

Horizontal slicing separates code by technical responsibility into layers. Layered Architecture is the canonical example.

src/
  controllers/
    UserController
    OrderController
    PaymentController
  services/
    UserService
    OrderService
    PaymentService
  repositories/
    UserRepository
    OrderRepository
    PaymentRepository

Code with the same technical role lives in the same directory. Controllers with controllers, services with services.

Strengths

Technical concerns are clearly separated. HTTP handling lives only in the Controller layer. Data access lives only in the Repository layer. Swapping a layer is straightforward — replacing a REST API with gRPC only touches the Controller layer.

The barrier to entry is low. Most frameworks (Spring, Express, Django) default to this structure. New team members grasp the layout quickly.

Limitations

Changing “order cancellation” touches OrderController, OrderService, OrderRepository, and possibly PaymentService. The change spans multiple layers.

As features grow, each layer accumulates dozens of files. services/ ends up with UserService, OrderService, PaymentService, NotificationService, InventoryService, ShippingService side by side. Same technical layer, entirely different domain contexts.

Vertical Slicing

Vertical slicing separates code by feature or domain. Each slice contains all the layers it needs.

src/
  user/
    UserController
    UserService
    UserRepository
  order/
    OrderController
    OrderService
    OrderRepository
  payment/
    PaymentController
    PaymentService
    PaymentRepository

Everything related to “orders” lives in order/. Modifying order features does not require touching other directories.

Vertical Slice Architecture

General vertical slicing divides code by domain module (User, Order, Payment). Jimmy Bogard’s Vertical Slice Architecture goes further, splitting by individual use case rather than module.

src/
  features/
    CreateOrder/
      CreateOrderHandler
      CreateOrderRequest
      CreateOrderValidator
    CancelOrder/
      CancelOrderHandler
      CancelOrderRequest
    GetOrderDetail/
      GetOrderDetailHandler
      GetOrderDetailQuery

Each use case is an independent slice. CreateOrder and CancelOrder belong to the same “order” domain but exist as separate slices. Within a single slice, the entire flow from request handling to data access is self-contained.

Strengths

Feature independence is high. Modifying one slice does not affect others. Code review scope narrows, and merge conflicts decrease. This structure suits multiple teams working independently in the same codebase.

Change cohesion is high. A single feature change completes within a single directory. Code review scope narrows, and changes do not affect other domains.

Limitations

Shared code management is tricky. Cross-cutting concerns like authentication, logging, and transaction handling are needed identically across multiple slices. Copying them into each slice creates duplication. Extracting shared modules introduces inter-slice dependencies, weakening independence. Judging the line between “acceptable duplication” and “excessive duplication” is necessary.

Consistency maintenance has a cost. As slices change independently, coding styles, error handling, and logging patterns may diverge across slices. Team conventions and code reviews keep them aligned.

Selection Criteria

This is not about choosing one over the other. The appropriate direction depends on the situation.

Early-stage projects with a small domain suit horizontal slicing. Few features mean few files per layer, and the framework’s default structure can be used as-is. The cost of setting up the architecture is low.

As features multiply and teams grow, vertical slicing’s advantages emerge. Independent work per feature becomes possible, and the scope of each change shrinks. When transitioning to microservices, vertically sliced code maps naturally to service boundaries.

Hybrid approaches are common. The top level is split vertically by domain, and within each domain, code is organized horizontally by technical layer.

src/
  order/
    controller/
    service/
    repository/
  payment/
    controller/
    service/
    repository/

Vertical slicing defines domain boundaries. Horizontal slicing organizes code within each domain. A structure frequently encountered in practice.


Neither approach is universally better — the appropriate direction depends on project scale and team structure. Horizontal slicing works well when starting small. As features multiply and teams grow, defining domain boundaries through vertical slicing is a natural progression.