Article Type: Engineering Standard
Audience: Customer Science software engineers, architects, automation developers, data engineers, support engineers, delivery engineers, and technical reviewers
Applies To: Application code, scripts, automations, integrations, APIs, frontend applications, backend services, data pipelines, CI/CD, testing, security, logging, and documentation
This Customer Science coding standard defines the expected quality, structure, readability, security, and maintainability practices for software and automation work. It is intended to make systems easier to build, review, operate, troubleshoot, extend, and hand over between teams.
The standard is deliberately technology-neutral. Individual projects may add more specific implementation rules, but project-specific rules should not weaken the principles in this document.
The purpose of this standard is to ensure that solutions are:
Use this document as a baseline for new development and as a review checklist when changing existing systems. When editing mature code, prefer consistency with the surrounding file unless the existing style creates a clear defect, security issue, or operational risk.
Code should be written for people first and machines second. Avoid clever shortcuts, dense expressions, hidden side effects, and overly abstract designs where a direct approach would be easier to understand. A reviewer should be able to identify what the code does, why it exists, and what could fail without mentally unpacking unnecessary complexity.
Prefer explicit control flow for validation, branching, error handling, retries, and resource cleanup. Compact syntax is acceptable only when it improves readability without hiding behavior.
Each layer, module, class, function, workflow, or script should have a clear responsibility. Do not mix request handling, business rules, persistence, external service calls, presentation logic, and infrastructure concerns in the same unit unless the unit is intentionally small and has no meaningful reuse or risk.
The boundary between layers should make testing and replacement easier. Business logic should not depend on transport-specific objects, user interface state, database connection objects, or cloud SDK details unless the component's purpose is specifically to adapt to that dependency.
Security must be considered at design time, not added only during release. Secrets must not be committed to source control. Sensitive values must be held in approved secret stores or secure runtime configuration. Access should be least-privilege, and every privileged operation must have an explicit authorization decision.
Inputs from users, files, queues, APIs, databases, and external services must be treated as untrusted until validated. Do not rely on frontend validation, naming conventions, or assumed upstream correctness as the only control.
Production systems must emit enough information to understand what happened, when it happened, what component was involved, which operation was attempted, and whether it succeeded. Logging should support investigation without exposing secrets or unnecessary sensitive data.
Long-running jobs, bulk operations, and integrations should log progress at useful intervals. Logs should include stable identifiers, operation names, counts, durations, and error categories where appropriate.
When an error occurs, the system should fail in a controlled manner. User-facing and caller-facing messages should be safe, concise, and actionable. Internal logs may contain diagnostic detail, but must not expose secrets, tokens, full connection strings, private keys, or sensitive payloads.
Retries should be used only for failures that are likely to be transient. Permanent validation failures, authorization failures, and malformed requests should fail quickly with clear handling.
Projects should be organised so that a new maintainer can quickly find source code, tests, configuration templates, deployment instructions, operational documentation, and build scripts. The exact folder names may vary by framework, but the purpose of each area should be obvious.
Avoid scattering related behavior across unrelated locations. Keep implementation code, generated output, local tooling, runtime artifacts, and documentation separated. Do not rely on personal IDE settings or local machine state for normal build and test workflows.
| Document | Purpose | Expectation |
|---|---|---|
| Overview | Explain what the system does and who owns it | Include purpose, high-level behavior, support ownership, and operational context. |
| Setup guide | Enable local or deployment setup | Include prerequisites, configuration, build commands, test commands, and common issues. |
| Configuration reference | Define runtime settings | Document setting names, purpose, default behavior, allowed values, and security notes. |
| Operational notes | Support production use | Include logging locations, troubleshooting steps, scheduling, monitoring, and rollback considerations. |
| Change history | Track meaningful changes | Summarise user-visible, operational, security, and compatibility-impacting changes. |
Configuration files committed to source control must contain safe defaults only. Sample files should be runnable in a development environment without exposing actual credentials. Do not commit secrets, production endpoints, organisation-specific credentials, private keys, or temporary access tokens.
Any new configuration setting should be documented at the same time it is introduced. If the setting affects performance, security, scheduling, data retention, or external connectivity, include operational guidance.
Names should describe intent, domain meaning, and responsibility. A good name reduces the need for comments because it tells the reader what the value or behavior represents. Avoid names that describe only implementation mechanics unless the implementation detail is the important concept.
| Element | Preferred Style | Guidance |
|---|---|---|
| Types and classes | PascalCase | Name by responsibility or domain concept. Avoid vague suffixes unless they describe a recognised pattern. |
| Methods and functions | PascalCase or language-standard function style | Use a verb phrase that describes the action and result. Async methods should follow the language or framework convention. |
| Variables and parameters | Language-standard local variable style | Name by meaning, not by type. Avoid single-letter names except for narrow, conventional loops or mathematical code. |
| Constants | Language-standard constant style | Name by the business or technical rule represented, not by the literal value. |
| Configuration keys | Hierarchical and descriptive | Group related settings and use names that make operational purpose clear. |
| Log properties | Stable and descriptive | Use consistent names for identifiers, operation names, durations, counts, and statuses. |
Avoid names that force readers to inspect implementation details to understand meaning. Generic names may be acceptable in very small scopes, but they should not cross method, module, component, service, or persistence boundaries.
Systems should separate transport concerns, validation, authorization, business rules, persistence, external service integration, and presentation. The exact layers may vary by technology, but dependencies should flow in one direction and business logic should remain testable without requiring live infrastructure.
Controllers, routes, handlers, pages, workflow triggers, and automation entry points should be thin. They should validate input shape, establish context, call the appropriate service or workflow, and translate the result into the correct response format.
Business logic should live in components that can be exercised directly by tests. It should not depend on HTTP request objects, UI state, database sessions, cloud SDK clients, file paths, or environment variables unless the component is specifically an adapter for that boundary.
When business rules become complex, make them explicit. Prefer named methods, domain models, validators, and policy objects over deeply nested conditionals or duplicated checks.
Persistence and external integrations should be isolated behind clear interfaces or modules. Query construction, API request formatting, retry handling, pagination, rate limiting, and response mapping should not leak into unrelated business logic.
Use structured APIs, parameterised queries, SDK models, serializers, and schema validation where available. Avoid ad hoc string manipulation for SQL, JSON, XML, URLs, paths, or protocol messages when safer library support exists.
C# code should follow the conventions used by the .NET ecosystem and by the file being edited. The default position is to use project formatter and editor configuration rules, four-space indentation, Allman braces, explicit visibility, and readable member ordering.
Formatting should make control flow obvious. Avoid compressed one-line logic, hidden side effects, and spacing that makes conditions or method chains hard to scan.
Use standard C# naming conventions. Types and public members use PascalCase. Local variables and parameters use camelCase. Private fields use a consistent private-field convention within the project. Constants should follow the existing project convention.
Names should communicate purpose and domain meaning. Avoid unnecessary abbreviations and avoid names that only repeat the type.
Use language features when they improve clarity. Do not use newer syntax only because it is shorter. A reviewer should be able to understand the important type, ownership, lifetime, and side-effect information without following several layers of inference.
nameof(...) instead of string literals for member and parameter names.var only when the type is obvious from the right-hand side.this. unless needed to resolve ambiguity.Classes should be organised so dependencies, public surface area, and implementation details are easy to find. Put fields near the top, followed by constructors, public members, and private helpers. Keep database access, API calls, transformation logic, and orchestration in separate components where possible.
Use immutable or readonly dependencies where practical. Avoid public mutable fields. Keep constructors simple and avoid doing expensive work before the object is used.
Most integration and application work is I/O-bound. Async code should remain async through the call path. Blocking on async work can exhaust thread pools, deadlock callers, and cause slow behavior under load.
async and await consistently.Catch exceptions only when the method can add context, translate the error into a safe contract, retry a transient dependency failure, or perform cleanup. Do not catch and silently continue unless skipping the work item is deliberate and logged.
When logging exceptions, include stable operational identifiers, operation names, table or resource names, counts, durations, and date ranges where appropriate. Do not log secrets, tokens, full connection strings, private keys, raw customer payloads, or unnecessary sensitive content.
Configuration belongs at the application boundary and should be passed into services as typed options or explicit dependencies. This keeps business logic testable and prevents hidden production differences between environments.
Do not hardcode values that can change between environments, organisations, regions, deployments, credentials, endpoints, feature flags, retry settings, timeouts, schedule intervals, or retention policies.
Python code should be formatted and linted by project-standard tools. Formatting should be automated so code reviews focus on design and behavior rather than whitespace.
Imports should be grouped and sorted consistently. Keep standard library imports, third-party imports, and local imports clearly separated. Avoid wildcard imports. Avoid importing heavy dependencies at module import time if doing so slows startup or complicates testing.
Entry points should validate request shape, establish execution context, call a service, and return a safe response. They should not contain business rules, persistence logic, or complex orchestration.
Caller-facing errors should use a stable response contract. Raw exceptions, stack traces, internal paths, dependency messages, and sensitive details must not be returned to external callers.
Services should contain business logic and orchestration decisions. They should receive dependencies through constructors or explicit parameters so tests can replace external systems with deterministic fakes.
Services should validate important assumptions and raise meaningful domain or application errors. They should avoid reading environment variables directly unless their purpose is configuration loading.
Repository, client, gateway, or adapter components should isolate database and external service details. They should own query construction, pagination, request formatting, response parsing, and dependency-specific retry behavior.
Use parameterised queries, SDK models, serializers, and schema validation. Avoid dynamic strings for queries, paths, and protocol payloads unless all inputs are safely encoded.
| Condition | Response Category | Guidance |
|---|---|---|
| Invalid input | Client error | Return a clear validation message that does not expose internals. |
| Unauthorized or forbidden action | Authorization error | Return a safe access message and log the decision context internally. |
| Dependency failure | Service error | Return a generic dependency failure message and log diagnostic details internally. |
| Unexpected exception | Server error | Return a generic fallback message and log the exception server-side. |
Frontend code should separate rendering, state management, routing, API access, validation, and reusable UI behavior. Pages and components should not directly duplicate authentication, authorization, telemetry, or API response parsing logic.
API wrapper modules should own URL construction, headers, request methods, response parsing, error translation, and telemetry around calls. Components should call wrapper methods rather than assembling authenticated requests directly.
Privileged routes and actions should be guarded in the frontend for user experience and in the backend for security. Frontend guards should fail closed, show clear feedback, and avoid rendering privileged controls when the user lacks access.
Workflows and automations should be modular, readable, recoverable, and observable. They should validate inputs early, handle expected business conditions explicitly, and separate business exceptions from system failures.
Avoid hardcoded paths, credentials, endpoints, environment values, email addresses, queue names, and approval rules. Use secure configuration and document operational dependencies.
Workflows should have a clear beginning, validation phase, processing phase, error path, notification or response path, and termination behavior. Long workflows should be decomposed into reusable subflows, activities, or modules.
Workflow, automation, variable, and action names should describe purpose and scope. Avoid names that depend on a specific customer, temporary project, or individual developer unless the system is intentionally customer-specific.
Logs should help operators understand behavior without needing to reproduce the issue immediately. Include enough stable context to correlate events across components while avoiding sensitive content.
Systems should expose meaningful operational signals such as success counts, failure counts, retry counts, processing duration, queue depth, dependency latency, and throughput. Metrics should be stable enough for alerting and dashboards.
Testing should focus on behavior, boundaries, and risk. Tests should give maintainers confidence that important workflows still behave correctly after change. Avoid tests that only repeat implementation details.
Coverage should scale with risk. Core business rules, authorization decisions, data transformations, retries, error handling, and integration boundaries should receive stronger coverage than trivial pass-through code.
Manual testing is acceptable for areas that cannot be automated immediately, but the steps and results should be recorded in the pull request or deployment notes. Repeated manual checks should be considered candidates for automation.
Pipelines should be understandable, repeatable, and auditable. Avoid hidden manual steps, undocumented environment assumptions, and machine-specific dependencies. Deployment jobs should make rollback and operational impact clear.
Every pull request should include enough information for a reviewer to understand the change without reconstructing intent from the diff alone.
A change is complete only when it is implemented, reviewed, verified, and ready to operate.
Document Version: 3.0
Last Updated: June 2026
Owner: Customer Science