Most of what makes a system hard to grow is decided in its first month, usually without anyone noticing a decision was made. The schema, the way modules call each other, and the route from a developer's laptop to production all set the cost of every later change. These choices are cheap to make carefully at the start. Teams tend either to skip them or to spend far too much on them, and both mistakes are avoidable.
Decisions that are expensive to reverse
Even a first release carries choices that are expensive to undo: the data model, how the front end talks to the back end, where the application runs, and how a change reaches production. Designing for scale does not mean designing for a crore of users on launch day. It means choosing so that the next step up does not require a rewrite. Four decisions do most of that work, and we usually settle them in the first week of a project.
Module boundaries inside one application
Microservices are the first thing people raise when scale comes up. For most young products, one deployable application with clear internal boundaries is easier to run with a team of four, and it can be split later along those same boundaries. A monolith is not the problem in itself. The problem is a monolith in which the invoicing code reaches into the inventory tables and the loyalty module reads straight from orders, so that nothing can be moved without moving everything. The useful test is whether each module owns its data or merely holds a reference into someone else's.
Product logic behind an API
If the product logic sits behind an API, a mobile app for field sales, a partner integration and next year's new front end can all use the same core. Writing the API also forces the team to state what each part is allowed to do, which is a design review conducted in code.
Database chosen by the shape of the data
Relational databases such as PostgreSQL and MySQL suit structured data with relationships and reporting needs, which covers orders, GST invoices and anything the accountant will ask about at month end. Document stores such as MongoDB or DynamoDB suit data whose shape varies or whose access pattern is simple and known, a product catalogue with different attributes per category being the usual example. Plenty of products use both for different jobs. Choose by the queries you expect to run, and write the indexes alongside the schema.
Deployment and monitoring before launch
Logs, error tracking and an automated deployment pipeline cost very little before launch. Retrofitting them during the first real traffic spike, with requests failing and no logs to read, is a bad week for everyone involved.
How LIWeddings is layered
LIWeddings is a Long Island wedding vendor marketplace with community forums. It was rebuilt as a Laravel monolith rather than a set of services. The front end uses Blade and Livewire, so pages are rendered by the same application that holds the business logic.
Around that single application sit the pieces that carry load. Search runs through Laravel Scout with MeiliSearch or OpenSearch, so vendor queries do not hit the database directly. Redis queues, managed with Horizon, handle notifications, lead routing and image processing outside the web request. Response and fragment caching plus a CloudFront CDN keep pages fast. MySQL stores the data, and role policies control who can do what.
- CloudFront CDNstatic assets and cached pages
- Laravel appBlade, Livewire, role policies
- Redisqueues via Horizon, response and fragment cache
- SearchScout with MeiliSearch or OpenSearch
- MySQLprimary data
One Laravel application, with the heavy work moved to the layers around it.

The point to take from it is where the work went. Slow tasks went to queues, repeated reads went to cache and search, and static content went to the CDN. Each of those layers can be scaled or replaced without splitting the application, and the application itself remains one thing to deploy and one thing to debug.
Where early architecture goes wrong
Splitting into services too early is the mistake I argue against most often. Network calls, separate deployments and distributed debugging arrive before there is a team or a traffic level that needs them, and a four-person team ends up looking after twelve repositories. Splitting too late is the mirror image. A tangled monolith is cut along the wrong lines, the new services still share one database, and nothing has been separated in any sense that matters.
The database treated as a dumping ground is the next most common problem. There are no indexes and no migration strategy, and the monthly sales report runs against the live tables on a Monday morning. Slow work inside the web request belongs in the same category; image processing, email and third-party calls block the user when they could sit on a queue. Without monitoring, the first sign of any of this is a customer complaint.
The opposite error exists as well. Designing for a load that does not exist yet delays the product that would have created that load.
- One deployment
- Clear internal modules
- Split a module out when load or team size demands it
- Network and versioning cost from day one
- Boundaries guessed before usage is known
- Harder to debug with a small team
Both options can scale; they differ in what they cost while the product is small.
When to split a module out
A module earns its own service when something about it differs from the rest of the application in a way that one deployment cannot accommodate. Consider a regional bus operator selling tickets online. Seat search takes a burst of traffic in the weeks before a long weekend or a festival, while the operator's back-office reporting is used by a handful of staff at a steady rate. Scaling the whole application to absorb the search peak wastes capacity; scaling search on its own is a reasonable case for separating it.
Other signals are organisational. When a separate team owns a module and needs to release it on its own schedule, the shared deployment becomes a source of friction. When a module needs a different runtime or a different data store, keeping it inside the monolith starts to distort the monolith. Absent signals of this kind, splitting adds operational cost and buys nothing.
The split is cheap only if the boundary was drawn early. A module that already owns its tables and exposes its behaviour through a defined interface can be moved in weeks. One that shares tables with three other modules has to be disentangled first, which is the expensive part. How that kind of entanglement accumulates is covered in our piece on technical debt in long-term projects.
The first design review
The first review we hold on a new product covers the decisions above in a single sitting, and our software development projects start from its output. By the end of it, the team should be able to answer three questions. Which data does each module own, as opposed to read? Which work runs outside the web request? What will the team look at when the first production error occurs?
Start with the first question and draw the module boundaries on one page. Anything you cannot assign to a module is where the future coupling will come from. Arguing about it on a whiteboard now takes an afternoon; arguing about it in a migration script two years on, with the business twice the size and a live database underneath, takes a quarter.
From our work
LIWeddings
Planning your dream wedding on Long Island starts here!
Read the project story
