Project structure and naming conventions

The blueprint layout, directory prefixes and service naming that keep a project predictable.

A well-structured project is one where anyone can predict where a piece of code lives before opening a single file. This page describes the conventions that Zato projects follow - the directory layout, what belongs where, and how services are named.

The layout

The starting point is the project blueprint, the same structure the deployment tutorial works with:

myproject
├── config
│   ├── enmasse
│   │   └── enmasse.yaml         # Channels, connections, security - the environment as YAML
│   ├── python-reqs
│   │   └── requirements.txt     # Extra Python packages your services need
│   └── user-conf
│       └── myconf.ini           # Your own configuration values
├── impl
│   ├── scripts
│   │   └── run-container.sh     # Starts the environment
│   └── src
│       ├── api
│       │   ├── billing.py
│       │   └── employee.py
│       ├── common
│       │   └── models.py
│       └── util
│           └── formatting.py
└── testing
    └── tests
        └── test_billing.py

Configuration and code are kept apart - config holds everything declarative, impl/src holds everything programmable, and testing holds the tests. The whole tree lives in git and is the single source of truth for the environment, per the GitOps model that deployment describes.

What belongs in each directory

Directories under src start with one of eight prefixes, and the prefix states the role of the code inside:

PrefixWhat lives there
apiServices that external callers invoke - the entry points of your integrations
servicesSame role as api - use whichever name reads better in your project
channelServices bound to specific channel types, e.g. MLLP or WebSocket handlers
adapterServices that talk to one external system each, wrapping its API details
coreBusiness logic that is independent of any transport or external system
modelData models - the dataclasses that define inputs and outputs
commonCode shared by everything else - constants, base classes, shared models
utilHelper functions with no business meaning - formatting, parsing, conversions

The prefixes are not just documentation - hot deployment deploys them in dependency order, shared code first, so common, util and model are always in place before the api services that import them. A directory only needs to start with the prefix, so api_billing and api_hr both count as api.

The channel, adapter and core layers

The three middle prefixes express one pattern - an integration is a chain of three responsibilities, each in its own layer:

  • A channel service receives the request and knows about the transport - HTTP headers, HL7 wrapping, file encodings - and nothing about business rules
  • A core service makes the business decisions and knows nothing about transports or external systems
  • An adapter service calls one external system and knows its API's details - its endpoints, its error codes, its pagination - and nothing about why it is being called

The payoff is that each layer changes independently - a new transport is a new channel service in front of the same core, and swapping a CRM vendor replaces one adapter without touching the business logic. The REST adapter pattern is the ready-made building block for the adapter layer.

For a small project, the layers can be a single api directory with three modules. The point is the separation of concerns, not the number of directories.

Naming services

Service names are dotted, lowercase, with words separated by dashes:

# -*- coding: utf-8 -*-

from zato.server.service import Service

class GetCustomerDetails(Service):
    name = 'crm.customer.get-details'

    def handle(self):
        ...

The convention is <project or system>.<entity>.<action>:

  • crm.customer.get-details
  • billing.invoice.create
  • hr.employee.sync-all

Names shape everything downstream - they appear in logs, in the Dashboard, in REST channels and in self.invoke calls, so a consistent scheme makes an environment self-describing. Group by system or business area first, never by transport - crm.customer.get-details stays correct when the same service gains a second channel.

Class names mirror the last part of the service name - GetCustomerDetails for get-details - and one module holds the services of one entity or one narrow area, so api/customer.py is where every crm.customer.* service lives.

Naming everything else

The same spirit extends to the objects defined in enmasse and the Dashboard:

  • REST channels are named after what they expose, e.g. api.customer with the URL path /api/customer/{customer_id}
  • Outgoing connections are named after the system they reach, e.g. CRM or Billing API, because that is the name your code uses - self.rest['Billing API']
  • Security definitions are named after who uses them, e.g. api.customer.basic-auth or Partner X API Key

Learn more