How software actually gets built, one small commit at a time
Nobody is born knowing Git, code review or how a request reaches a database. Everyone on your team once typed git push and hoped for the best. This guide walks through the everyday parts of the job so they stop feeling mysterious.
Covers the tools many enterprise teams use: Java SQL MongoDB Python AWS CloudWatch Bitbucket Jira ConfluenceUsing GitHub, GitLab or another cloud instead? The ideas carry straight over.
What's inside: 11 short parts, 20–60 minutes each. Go at your own pace.
Start here
What software engineers actually do
Software is a set of instructions that tells computers what to do. Engineers write those instructions as code. But typing new code is a small part of the job. Most of it is understanding problems, reading existing code, working with people, and changing things safely.
You'll spend far more time reading code than writing it. That's normal, and it's how everyone learns a codebase.
Who's on a software team
| Role | What they do | You'll work with them when… |
|---|---|---|
| Backend engineer | Builds services, APIs and databases (often in Java) | You change how data is stored or processed |
| Frontend / mobile engineer | Builds the screens people use: web pages, the technician app | You change an API the app calls |
| QA engineer | Tests features and finds bugs before customers do | Your ticket moves to testing |
| DevOps / platform | Runs AWS, pipelines, monitoring and deployments | You need access, or a deploy fails |
| Product manager / owner | Decides what to build and why; writes and prioritises tickets | A ticket is unclear |
| Designer | Designs how screens look and behave | You build or change a screen |
| Tech lead / architect | Sets technical direction and reviews big decisions | You're unsure how to approach something |
| Support | Talks to customers and reports their problems | A bug comes in from a customer |
How work flows through the team
- A customer has a need or a problem.
- Product turns it into a ticket and decides when it gets done.
- Engineers design and build it, and review each other's code.
- QA tests it. It's released to customers.
- Customers use it, give feedback, and the cycle repeats.
A typical day
| When | What |
|---|---|
| Morning | Check messages and PR comments. Daily standup (15 minutes). |
| Late morning | Focus time: read code, write code, run tests. |
| After lunch | Review a teammate's PR. Maybe pair with someone on a tricky bug. |
| Afternoon | A meeting or two: refinement, design discussion, 1:1 with your lead. |
| End of day | Push your work, update your ticket, note where to pick up tomorrow. |
What's expected of you in your first months
Nobody expects you to know everything. They expect you to learn steadily, ask questions, be honest about progress, and ship small, reliable changes. That's it. Speed comes later, by itself.
- Engineering is mostly understanding problems and reading code, not just typing it.
- Know who does what on your team, so you know who to ask.
- Early on, aim to learn, ask and ship small things reliably.
Terminal basics
The terminal (also called the command line or shell) is a window where you control the computer by typing commands instead of clicking. Git, Maven, Python scripts and the AWS CLI all run here, so it's worth getting comfortable early.
- Mac: the Terminal app (or iTerm2). It runs a shell called zsh.
- Windows: use Git Bash or WSL so commands match this guide. PowerShell uses different commands.
- IntelliJ and VS Code also have a built-in terminal at the bottom of the window.
Folders and paths
Your computer is a tree of folders (also called directories). The terminal is always "standing" in one folder, the current directory. A path describes where something is:
~is your home folder, e.g./Users/you..is the current folder, and..is the folder above it./Users/you/workorder-serviceis an absolute path (from the very top);workorder-service/srcis a relative path (from where you are).
Type a command and press Enter. Type help to see what works. Use the Up arrow to repeat a command and Tab to finish a name.
Commands to know
| Command | What it does |
|---|---|
pwd | Print where you are |
ls / ls -la | List files / including hidden ones, with details |
cd folder | Go into a folder. cd .. goes up, cd ~ goes home |
mkdir name | Make a new folder |
touch file.txt | Create an empty file |
cat file / less file | Show a file / page through a long file (press q to quit) |
cp a b / mv a b | Copy / move or rename |
rm file | Delete a file. There is no trash bin: it's gone |
clear | Clear the screen |
history | Show commands you've typed |
Keyboard shortcuts that save time
- Tab finishes file and folder names for you. Press it constantly.
- ↑ brings back the previous command.
- Ctrl+C stops a running command (like a server or a stuck script).
- Ctrl+R searches your command history.
- Names with spaces need quotes:
cd "SWE tutorials".
pwd,lsandcdare how you find your way around.- Tab completion and the Up arrow do half the typing for you.
- Read what the terminal prints. Errors usually say exactly what's wrong.
Your tools and first-week checklist
Your first week is mostly about getting access, installing tools, and getting the project running on your laptop. It's slow for everyone, and that's fine. Here are the tools you'll likely use, and a checklist to track your progress.
| Tool | What it's for |
|---|---|
| IntelliJ IDEA | The main editor (IDE) for Java: code, run, debug, test |
| VS Code | A lighter editor, handy for Python scripts and config files |
| JDK and Maven | Java itself, and the tool that builds Java projects |
| Git | Version control (see Git in plain words) |
| Python 3 | For team scripts |
| Docker | Runs databases and services locally in containers |
| Postman | Sends API requests while you develop |
| AWS CLI | Talks to AWS from the terminal, e.g. to read logs |
| MongoDB Compass / a SQL client | Browse databases visually (e.g. DBeaver or DataGrip for SQL) |
| Jira, Confluence, Bitbucket | Tickets, documentation and code (Atlassian) |
| Slack or Teams | Team chat |
Your team's exact list may differ. Ask your lead or onboarding buddy for theirs.
- Install the exact versions the README asks for.
- Your first real win is building and running the project locally.
- Write down every setup problem you hit, and fix the docs for the next person.
How teams work
How a piece of work travels from idea to users
Every feature or bug fix you touch follows roughly the same path. Teams use different tools, but the shape stays the same. Once you know the path, you always know what comes next and who to ask.
This path is often called the software development lifecycle (SDLC). You'll hear the word in interviews and planning meetings. It just means "the steps work goes through."
What "done" means
Many teams agree on a Definition of Done: a checklist every ticket must pass before anyone calls it finished. A typical one:
- Code is reviewed and approved by at least one teammate.
- Automated tests are written and passing.
- Documentation (README, doc comments, API docs) is updated if behaviour changed.
- The change is merged and deployed to at least a test environment.
- The ticket's acceptance criteria are all met.
If your team has one, find it in the wiki and keep it open while you work.
- Work starts from a ticket. Understand the "why" before writing code.
- Your code reaches users only after review, automated checks and a merge.
- Small changes move through this path faster and with less stress.
Agile, Scrum and the team's rhythm
Agile is an approach where teams build software in small pieces, show it often, and adjust based on feedback. It replaced the old style of planning everything for a year and delivering it all at the end.
Scrum is the most common way to do Agile. Work happens in sprints, fixed cycles usually two weeks long. Each sprint has the same set of meetings, called ceremonies.
Anatomy of a good ticket
Tickets are often written as user stories: "As a type of user, I want something, so that a benefit." Below the story are acceptance criteria, the testable conditions that prove the work is done.
PROJ-142 Add "Remember me" to login Story As a returning customer, I want to stay signed in on my own laptop, so that I don't type my password every day. Acceptance criteria ✓ A "Remember me" checkbox appears under the password field ✓ When checked, the session lasts 30 days instead of 1 day ✓ Signing out ends the session even if "Remember me" was checked Estimate 3 points Labels frontend, auth
Story points and estimates
Teams estimate effort in story points (often 1, 2, 3, 5, 8, 13) instead of hours. Points describe relative size and uncertainty: a 5 is "bigger and less clear than a 2". Your early estimates will be off. Everyone's are. Estimate honestly and say what you're unsure about.
Your standup update
The daily standup is 15 minutes, not a status report to a boss. Say what you did, what you'll do, and anything blocking you. Mentioning a blocker early is the most useful thing you can say.
Yesterday I worked on stuff. Today I'll continue. No blockers.
Yesterday: login checkbox UI done, PR open. Today: session length change on the API. Blocker: I don't have access to the staging database. Who can grant it?
- Agile means small pieces, shown often, adjusted by feedback.
- Acceptance criteria tell you exactly when a ticket is done. Read them first.
- Raise blockers at standup immediately. Waiting silently costs the whole team.
Jira and Confluence: where work and knowledge live
Atlassian makes three tools that many teams use every day, and they're linked together:
- Jira holds the work: tickets, sprints and boards.
- Confluence holds the knowledge: setup guides, architecture, runbooks, meeting notes.
- Bitbucket holds the code: repositories, pull requests and pipelines (see the Bitbucket chapter).
The glue is the issue key, like FSM-1234. Put it in your branch name, commit messages and pull request title, and Jira automatically shows the branch, commits and PR on the ticket's Development panel. Your manager can see progress without asking you.
Issue types
| Type | What it is | Example |
|---|---|---|
| Epic | A big goal made of many tickets, often weeks of work | Offline mode for the technician app |
| Story | A feature from the user's point of view | Technician can attach photos to a work order |
| Task | Technical work that isn't a user feature | Upgrade Spring Boot to 3.3 |
| Bug | Something that should work and doesn't | Work order list crashes when an asset has no address |
| Sub-task | A smaller piece of a story or task | Add photo upload endpoint |
Your board's column names may differ slightly. Ask which statuses your team uses.
Finding tickets with JQL
JQL (Jira Query Language) is search for Jira. Paste these into Filters → Advanced issue search, then save the useful ones.
-- Everything assigned to me that isn't finished assignee = currentUser() AND resolution = Unresolved ORDER BY priority DESC -- My tickets in the current sprint sprint in openSprints() AND assignee = currentUser() -- Open bugs in a project project = FSM AND type = Bug AND status != Done -- Anything mentioning the scheduler, changed in the last week project = FSM AND text ~ "scheduler" AND updated >= -7d
Smart commits
If your Jira admin has enabled them, commit messages can comment on a ticket or log time:
git commit -m "FSM-1234 Handle assets with no address #comment Fixed null city in routing #time 2h"
Using Confluence well
- Search before asking. Look for pages like "Local setup", "Onboarding", "Architecture", "Runbook" and "Release notes".
- Watch the pages you depend on (the eye icon) so you're notified when they change.
- Fix what's out of date. If a setup step was wrong, edit the page. New joiners are the best at spotting stale docs.
- Link both ways. Paste a Jira link into a Confluence page and it shows the live ticket status; link the page back from the ticket.
- Write things down for the next person. Solved something tricky? A short how-to page saves the next joiner a day.
- Jira is for work, Confluence for knowledge, Bitbucket for code.
- Put the issue key (like FSM-1234) in branches, commits and PR titles so everything links up.
- Keep your ticket status accurate, and search Confluence before asking.
Git basics
Git in plain words
Git is a version control system. It remembers every saved version of a project and lets many people change the same code without overwriting each other's work. Bitbucket (used in this guide's examples), GitHub and GitLab are websites that host a shared copy of that history and add pull requests, reviews and automation on top. The Git commands are identical on all of them.
Three words, explained simply
- Repository (repo): a project folder that Git is watching. It holds your files plus a hidden
.gitfolder where every past version is kept. When people say "clone the repo", they mean "download the project and its history". - Commit: a snapshot of your changes with a short message ("Fix crash when cart is empty"), your name and the time. Each commit gets a unique ID like
a1b2c3d, so anyone can point to it. - Remote: the copy of the repo on Bitbucket. Your laptop and Bitbucket each have a full copy;
pushandpullkeep them in sync.
Why not just save files normally? Because a normal save overwrites the old version. With Git you can always answer "what changed, who changed it, when, and why?", and you can go back if the new version is broken.
First-time setup
You do this once per computer, then once per project.
# Tell Git who you are (shows on every commit) git config --global user.name "Asha Rao" git config --global user.email "asha@company.com" # Download a project and its whole history git clone git@bitbucket.org:your-team/workorder-service.git cd workorder-service
The four places a change lives
Most beginner confusion comes from not knowing which place a change is in.
Working folder
Files on your laptop as you edit them.Staging area
Changes you picked for the next save point.Local history
Save points (commits) on your laptop only.Remote (Bitbucket)
The shared copy your team can see.Start by editing a file.
What never goes into Git: .gitignore
A file called .gitignore lists things Git should pretend don't exist. Check it before your first commit in any project.
# .gitignore node_modules/ # installed libraries, re-downloadable build/ # generated output .env # passwords and API keys: never commit these *.log .DS_Store # Mac clutter .idea/ .vscode/ # personal editor settings
Reading history
git log --onelinelists recent commits, one per line.git diffshows changes you haven't staged.git diff --stagedshows what you're about to commit.git show a1b2c3dshows one commit in full.git blame file.jsshows who last changed each line and in which commit. Use it to find context, not culprits.
git addpicks changes.git commitsaves them locally.git pushshares them.- A commit on your laptop is invisible to the team until you push.
- When unsure, run
git status. It's always safe and tells you where everything is.
Branches, and how a team repo uses them
main is the branch everyone trusts. It should always build and pass its tests. Nobody writes new work directly on it. You create your own branch, make your commits there, and bring them back into main through a pull request.
Most repos protect main with branch protection rules: it can't be pushed to directly, and a pull request needs an approval and passing checks before it can merge. If a push to main is rejected, that's the rule working, not you breaking something.
Merging just means taking the commits from one branch and adding them to another. When your feature branch is merged into main, your work becomes part of the official code that everyone builds on and that eventually ships to customers.
You are on main
Common branch names
Most teams follow a naming pattern so anyone can tell what a branch is for. Check your repo's CONTRIBUTING.md or ask your lead for the exact rules.
| Branch | What it's for | Who creates it |
|---|---|---|
main | Always working code. Protected, so changes arrive only through a reviewed PR. | Exists already |
develop | Some teams (using "Git Flow") collect finished features here before releasing to main. | Exists already, if used |
feature/PROJ-142-remember-me | New behaviour. Lives for days, not months. | You |
bugfix/PROJ-151-date-format | A fix for something broken that isn't urgent. | You |
hotfix/payment-timeout | An urgent fix for something broken in production. | Usually senior engineers |
release/2.4 | A frozen version for final testing before shipping. | Release owner |
Two common strategies optional
- Trunk-based / GitHub Flow. Short feature branches off
main, merged back within a day or a few days. Simple, and the most common today. - Git Flow. Adds
develop,release/*andhotfix/*branches. Used by teams that ship versioned releases on a schedule, such as mobile apps.
The daily routine
# 1. Start from the latest main git switch main git pull # 2. Make your own branch git switch -c feature/PROJ-142-remember-me # 3. Work in small steps git add src/LoginForm.tsx git commit -m "Add remember-me checkbox to login form" # 4. Share it and open a pull request on Bitbucket git push -u origin feature/PROJ-142-remember-me
Go deeper (optional): keeping your branch up to date
Keeping your branch up to date
While you work, teammates merge into main. Bring their changes into your branch every day or two, so surprises stay small. There are two ways:
git switch main git pull git switch feature/PROJ-142-remember-me git merge main
Adds a merge commit. History shows exactly what happened.
git fetch origin git rebase origin/main # branch already pushed? then: git push --force-with-lease
Replays your commits on top of the latest main. Only rebase branches that you alone work on.
- Never commit straight to
main. Always branch from a freshmain. - Name branches so a stranger knows what they are: type, ticket, short description.
- Short-lived branches, updated often, cause fewer merge conflicts.
Git with your team
Merge conflicts are normal
A merge conflict happens when two branches changed the same lines, and Git can't tell which version is right. Git stops and asks a human. Nothing is broken and nothing is lost. Every engineer resolves conflicts regularly.
Git marks the conflicting part of the file with three markers:
<<<<<<< HEAD: start of the version on the branch you're on.=======: the divider.>>>>>>> other-branch: end of the incoming version.
Your job: edit the file so it contains the correct final code, delete the three marker lines, then stage and commit.
You ran git merge main on your branch. Your teammate's branch added a logger import. Your branch added a retry import. Both are used further down.
Resolving step by step
- Run
git status. It lists files under "both modified". - Open each file. Most editors (VS Code, IntelliJ) highlight conflicts and offer "Accept current / incoming / both" buttons.
- Decide what the final code should be. Often it's a mix of both sides.
- Remove all marker lines. Run the app and tests.
git addeach fixed file, thengit commit(orgit rebase --continueduring a rebase).- Unsure which side is right? Ask the person who wrote the other change.
git logtells you who.
Want to give up and start over? git merge --abort puts everything back exactly as it was before the merge.
- A conflict is Git asking a question, not an error you caused.
- The right answer is often a combination of both sides. Read both before choosing.
git merge --abortis your safe exit. Pullingmainoften keeps conflicts small.
"Oops" — undoing mistakes in Git
Git almost never truly loses committed work. Most mistakes have a calm, two-command fix. Pick a situation below to see it.
git reset --hard throws away uncommitted work permanently, and git push --force can overwrite teammates' commits on a shared branch. If you think you need either, ask someone first. There's no shame in it; seniors double-check these too.Revert vs. reset
git revertmakes a new commit that undoes an old one. History stays intact. Safe on shared branches.git resetmoves your branch backwards, as if commits never happened. Fine for local commits nobody else has. Dangerous once pushed.
- Pushed something wrong? Use
git revert, not reset. git reflogremembers where your branch has been. It can rescue "lost" commits.- When in doubt, stop and ask before running any command with
--hardor--force.
Commit messages, pull requests and code review
Six months from now, someone will read your commit to figure out why a line exists. That person might be you. A good message saves them an hour.
A useful pattern: start with a verb, say what changes, keep the first line under about 70 characters. If the reason isn't obvious, add a blank line and a short paragraph explaining why.
fix wip changes final final v2 updated stuff
Fix crash when cart is empty Add retry to payment API call Rename getData to fetchOrders Remove unused date helper
Conventional Commits
Many teams prefix messages with a type. Tools then generate changelogs and version numbers automatically.
| Prefix | Meaning | Example |
|---|---|---|
feat: | New feature | feat: add remember-me option to login |
fix: | Bug fix | fix: handle empty cart in checkout total |
refactor: | Code change with no behaviour change | refactor: extract price calculation |
test: | Tests only | test: cover expired discount codes |
docs: | Documentation only | docs: explain local setup in README |
chore: | Tooling, dependencies, config | chore: bump eslint to 9.2 |
What a pull request (PR) is
A pull request (GitLab calls it a merge request) asks the team: "please review my branch and merge it into main." It's where code review and automated checks happen. A good description answers three questions:
## What Adds a "Remember me" checkbox. When checked, sessions last 30 days. ## Why Returning customers asked to stay signed in. Ticket: PROJ-142 ## How I tested - Unit tests for session length (new: session.test.ts) - Manually: signed in with and without the box, restarted browser - Screenshot of the form attached below ## Notes for reviewers I wasn't sure whether to store the flag in a cookie or the DB. Went with the cookie. Happy to change.
Receiving review comments
Review comments are about the code, not about you. Senior engineers get lots of comments too. Reply to each one: change the code, or explain your reasoning politely. "Good catch, fixed in a3f9c1" is a perfectly good reply.
Reviewing other people's code
You'll be asked to review soon, even as a junior. Your fresh eyes are valuable. Ask questions freely. A simple checklist:
- Does it do what the ticket asks? Did I try it?
- Are there tests? Would they catch a regression?
- Are names clear? Could I understand this in six months?
- What happens with empty, null or very large input?
- Any passwords, keys or personal data in the diff?
Label comments so the author knows how important they are: nit: (tiny, optional), question: (I want to understand), suggestion: (consider this), blocking: (must fix before merge). Always say what you liked, too.
- Commit messages start with a verb and say what changed.
- Keep PRs small. Under about 300 changed lines gets reviewed faster and better.
- Treat review comments as free mentoring, and review others kindly and specifically.
Bitbucket, day to day
Bitbucket Cloud is where many teams keep their repositories, pull requests and pipelines. It's tightly linked with Jira, so if your team uses both, most of your daily flow starts from a ticket and ends with a merged PR that updates that ticket.
Merge strategies
When you click Merge, Bitbucket asks how. Your team probably has a default; use it.
| Strategy | What ends up on main | Good for |
|---|---|---|
| Merge commit | All your commits, plus one merge commit | Keeping the full story of how the work happened |
| Squash | All your commits combined into one | A tidy main branch with one commit per ticket |
| Fast-forward | Your commits added in a straight line, no merge commit | Linear history. Only works if your branch is up to date with main |
Signing in from the command line
- SSH (recommended): generate a key once, add the public half to Bitbucket under Personal settings → SSH keys, then clone with the
git@bitbucket.org:…URL. No passwords after that. - HTTPS: use an API token from your Atlassian account settings, never your Atlassian password. Older guides mention "app passwords"; Atlassian has replaced them with API tokens.
- Never share your private key (
id_ed25519, the file without.pub) with anyone, including in tickets or chat.
Bitbucket Pipelines
Your repo's CI lives in a file called bitbucket-pipelines.yml at the root. It decides which builds and tests run on each PR and branch. The shipping chapter walks through an example. When a pipeline fails, click into the failed step and read the log from the first red line.
- Create branches from the Jira ticket so the key is in the name and everything links.
- A PR merges only with approvals, resolved tasks and a green pipeline.
- Use SSH keys or API tokens. Your private key and password never leave your laptop.
Java and Python
Java essentials for backend work
Most enterprise backend services are written in Java, usually with Spring Boot and built with Maven or Gradle. You don't need to know all of Java on day one. You need to find your way around a service and make a safe change.
What is Spring Boot?
A framework is ready-made code that handles the boring, repetitive parts of an app, so you only write the parts specific to your business. Spring Boot starts a web server, turns incoming HTTP requests into Java method calls, turns Java objects into JSON responses, and connects to databases. You write the logic: "when a work order is created, check the asset exists and assign a technician".
Two Spring ideas confuse almost every beginner, so here they are in plain words:
- Annotations are the
@Somethinglabels above classes and methods. They're instructions to Spring.@GetMapping("/{id}")means "call this method when a GET request arrives at this path". - Dependency injection means you don't create the objects you need yourself. You list them in your constructor, and Spring hands them to you. That makes it easy to pass in a fake version during tests.
How a typical service is laid out
workorder-service/ ├── pom.xml # dependencies and build settings (Maven) ├── src/main/java/com/example/workorder/ │ ├── WorkOrderApplication.java # starts the app │ ├── api/WorkOrderController.java # HTTP endpoints │ ├── service/WorkOrderService.java # business rules │ ├── repository/WorkOrderRepository.java # database access │ └── model/WorkOrder.java # data classes ├── src/main/resources/application.yml # configuration └── src/test/java/com/example/workorder/ # tests mirror the main folders
The three layers
A request flows down through the layers and the answer flows back up. Each layer has one job, so you always know where a change belongs.
| Layer | Job | Should not… |
|---|---|---|
| Controller | Receives HTTP requests, validates input, returns responses and status codes | Contain business rules or SQL |
| Service | Business rules: "a closed work order can't be reassigned" | Know about HTTP details |
| Repository | Reads and writes the database (SQL or MongoDB) | Make business decisions |
@RestController @RequestMapping("/api/work-orders") public class WorkOrderController { private final WorkOrderService service; // Spring passes the service in for us ("dependency injection") public WorkOrderController(WorkOrderService service) { this.service = service; } @GetMapping("/{id}") public WorkOrder get(@PathVariable long id) { return service.findById(id); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public WorkOrder create(@Valid @RequestBody NewWorkOrder request) { return service.create(request); } } // In WorkOrderService: Optional avoids returning null public WorkOrder findById(long id) { return repository.findById(id) .orElseThrow(() -> new NotFoundException("Work order " + id + " not found")); }
Core Java ideas you'll see daily
| Concept | In plain words |
|---|---|
| Class and object | A class is a blueprint (WorkOrder); an object is one real instance (work order 88213). |
| Interface | A promise of which methods exist, without the code. Lets you swap implementations, e.g. a fake repository in tests. |
| Collections | List (ordered), Map (key → value), Set (no duplicates). See the Big-O chapter for choosing. |
| Generics | List<WorkOrder> means "a list that only holds work orders". The compiler checks it for you. |
| Exceptions | How Java reports errors. Checked ones must be handled or declared; unchecked ones (like NullPointerException) usually mean a bug. |
| Optional | A box that may be empty. Forces you to handle "not found" instead of returning null. |
| Streams | orders.stream().filter(o -> o.isOpen()).toList(): transform collections in a readable pipeline. |
| Annotations | The @Something labels. They tell Spring what a class or method is for. |
The classic Java gotcha: comparing strings
// == compares memory addresses if (status == "OPEN") { ... }
// equals compares the text if ("OPEN".equals(status)) { ... } // better still: an enum if (status == Status.OPEN) { ... }
Handling exceptions
An empty catch block is the most common Java mistake in code reviews. It hides the error, so the bug shows up later somewhere confusing.
try { routing.assign(order); } catch (Exception e) { // nothing here: the failure vanishes }
try { routing.assign(order); } catch (RoutingException e) { log.warn("Could not route work order {}", order.getId(), e); throw new ConflictException( "No technician available", e); }
Build commands
| I want to… | Maven | Gradle |
|---|---|---|
| Build and run all tests | mvn clean install | ./gradlew build |
| Run tests only | mvn test | ./gradlew test |
| Run one test class | mvn test -Dtest=WorkOrderServiceTest | ./gradlew test --tests WorkOrderServiceTest |
| Start a Spring Boot app | mvn spring-boot:run | ./gradlew bootRun |
| See where a library comes from | mvn dependency:tree | ./gradlew dependencies |
- Controller handles HTTP, service holds business rules, repository talks to the database.
- Compare strings with
.equals(), and preferOptionalover returningnull. mvn testbefore every push. Learn to run one test class on its own.
Running Python scripts safely
Teams keep Python scripts for one-off and repeated jobs: fixing bad data, exporting reports, calling AWS, running migrations. You'll often be asked to run a script someone else wrote. Doing that safely is a skill.
What a well-behaved script looks like
Read a script before you run it. Good ones explain themselves, require you to choose an environment, and support a dry run.
#!/usr/bin/env python3 """Close work orders that have been COMPLETED for 30+ days. Usage: python close_stale_orders.py --env dev --dry-run """ import argparse, os, sys from datetime import datetime, timedelta, timezone from pymongo import MongoClient def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--env", choices=["dev", "staging", "prod"], required=True) parser.add_argument("--dry-run", action="store_true", help="show changes, change nothing") args = parser.parse_args() db = MongoClient(os.environ["MONGO_URI"])["fieldservice"] cutoff = datetime.now(timezone.utc) - timedelta(days=30) query = {"status": "COMPLETED", "completedAt": {"$lt": cutoff}} count = db.workOrders.count_documents(query) if args.dry_run: print(f"[dry run] would close {count} work orders in {args.env}") return 0 result = db.workOrders.update_many(query, {"$set": {"status": "CLOSED"}}) print(f"Closed {result.modified_count} work orders in {args.env}") return 0 if __name__ == "__main__": sys.exit(main())
Errors you'll hit, and what they mean
| Error | Usual cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'pymongo' | Virtual environment not active, or packages not installed | source .venv/bin/activate then pip install -r requirements.txt |
python: command not found | Your system only has python3 | Use python3, or activate the venv (it provides python) |
KeyError: 'MONGO_URI' | A required environment variable isn't set | export MONGO_URI=… or fill in the .env file the README describes |
NoCredentialsError / ExpiredToken | AWS login missing or expired | aws sso login --profile dev |
Permission denied | The file isn't marked executable | Run it as python3 script.py instead |
SyntaxError on f-strings or type hints | Python version too old | Check python3 --version against the README |
- One virtual environment per project. Activate it before installing or running anything.
- Read the script and its
--helpbefore running it. - Dry run first, dev before prod, and get approval for production changes.
Databases
SQL and joins
A relational database stores data in tables. Each row is one record, each column one field. Every table has a primary key (a unique ID). A foreign key is a column that points to another table's primary key. That's how tables relate.
For example, a work_orders table might have a technician_id column. The value 58 in that column means "this work order belongs to the technician whose id is 58 in the technicians table". Instead of copying the technician's name into every work order, you store their ID once and look it up when needed.
You'll also see NULL, which means "no value at all", not zero and not an empty string. A work order with technician_id = NULL simply hasn't been assigned yet. To find those, write WHERE technician_id IS NULL; = NULL never matches anything.
The statements you'll use 90% of the time
SELECT id, title, status -- which columns FROM work_orders -- which table WHERE status = 'OPEN' -- which rows ORDER BY created_at DESC -- newest first LIMIT 20; -- How many open orders does each technician have? SELECT technician_id, COUNT(*) AS open_orders FROM work_orders WHERE status = 'OPEN' GROUP BY technician_id HAVING COUNT(*) > 5; -- HAVING filters groups, WHERE filters rows INSERT INTO work_orders (title, status) VALUES ('Inspect pump', 'OPEN'); UPDATE work_orders SET status = 'CLOSED' WHERE id = 104; DELETE FROM work_orders WHERE id = 104;
Joins: combining tables
A JOIN matches rows from two tables using a shared value, usually a foreign key. The join type decides what happens to rows that have no match.
| id | name |
|---|
| id | title | technician_id |
|---|
| name | title |
|---|
SELECT with the exact same WHERE first and check the row count. An UPDATE without WHERE changes every row in the table. On shared databases, wrap changes in a transaction: BEGIN; … check … COMMIT; (or ROLLBACK; to undo).Indexes, briefly
An index is like the index at the back of a book: it lets the database find rows without reading the whole table. Columns you filter or join on often (technician_id, status) usually need one. If a query is slow, run EXPLAIN before it to see whether an index is used.
- INNER JOIN keeps only matches; LEFT JOIN keeps every row from the left table and fills gaps with NULL.
WHEREfilters rows,GROUP BYgroups them,HAVINGfilters groups.- Always SELECT before you UPDATE or DELETE, and never run them without a WHERE.
MongoDB: documents instead of tables
MongoDB stores documents: JSON-like objects that can contain nested objects and arrays. Documents live in collections. Two documents in the same collection don't have to have exactly the same fields, which makes MongoDB good for data whose shape varies, such as forms that differ per customer.
What is JSON?
JSON is a simple text format for data that both people and programs can read. You'll see it everywhere: in MongoDB, in API requests and responses, and in config files. It has only a few building blocks:
{ }an object: a set of"name": valuepairs, like one work order.[ ]an array: an ordered list, like a list of notes.- Values are text in quotes (
"OPEN"), numbers (2),true/false,null, or another object or array.
A MongoDB document is basically a JSON object, with a few extra types such as dates and IDs.
// One document in the workOrders collection { "_id": ObjectId("66f1c2a9e4b0a1d2c3e4f5a6"), "number": "WO-88213", "status": "OPEN", "priority": 2, "technicianId": 58, "asset": { "id": "MTR-4471", "type": "meter", "city": "Austin" }, // nested object "notes": [ // array { "by": "dispatcher", "text": "Gate code 4410" } ], "createdAt": ISODate("2026-09-30T09:14:02Z") }
| SQL word | MongoDB word |
|---|---|
| Database | Database |
| Table | Collection |
| Row | Document |
| Column | Field |
| Primary key | _id (added automatically) |
| JOIN | $lookup, or embed the related data inside the document |
Run these in mongosh (the MongoDB shell) or in the MongoDB Compass app.
SQL or MongoDB?
- SQL fits data with clear relationships and strict consistency: billing, inventory, users and permissions.
- MongoDB fits flexible or nested data read as a whole: a work order with its notes, photos and custom form answers.
- Many systems use both. Ask which data lives where in your services.
db.workOrders.updateMany({}, …) and deleteMany({}) with an empty filter touch every document. Run find() or countDocuments() with the same filter first.- MongoDB stores flexible JSON-like documents in collections.
find(filter, projection)reads;updateOne/updateManywith$setchange data;aggregategroups and joins.- Test any filter with
findorcountDocumentsbefore updating or deleting.
Writing clear code
Method-level comments and documentation
A method-level comment (also called a doc comment or docstring) sits right above or inside a function. It tells other developers how to use the function without reading its code. A complete one covers:
- Summary: one sentence on what the function does.
- Parameters: what each input means, its units, and whether it can be empty or null.
- Return value: what comes back and any guarantees ("never negative").
- Errors: which exceptions it throws and when.
- Side effects (if any): sends an email, writes to the database, changes a global.
Editors read these comments. When a teammate hovers over your function in their IDE, your comment pops up. Tools like Javadoc, Sphinx, JSDoc and DocFX also turn them into documentation websites.
Explain why, not what
The code already shows what happens. Comments are most useful when they explain intent, assumptions or surprises the code can't express.
// add 1 to page page = page + 1; // loop over users for (User u : users) { // TODO fix this
// The orders API counts pages from 1 page = page + 1; // Skip suspended users: billing has // already closed their accounts for (User u : activeUsers) { // TODO(PROJ-201): remove once v1 API is retired
Other kinds of documentation
| Kind | Where it lives | What it answers |
|---|---|---|
| Doc comment | Above a function or class | How do I use this function? |
| Inline comment | Next to a tricky line | Why is this line written this odd way? |
| README | Root of the repo | What is this project and how do I run it locally? |
| API docs (OpenAPI/Swagger) | Generated from code or a spec file | Which endpoints exist and what do they accept? |
| ADR (Architecture Decision Record) | docs/adr/ folder | Why did we choose this database or design? |
| Runbook | Team wiki | The service is down at 2am. What do I do? |
- Every public method deserves a doc comment: purpose, parameters, return value, errors.
- Comments explain why. Clear names explain what.
- When you change the code, update the comment in the same commit. A wrong comment is worse than none.
Clean code principles
Code is read far more often than it's written. "Clean code" just means code the next person can understand and change safely. These principles have names you'll hear in reviews.
| Principle | In plain words |
|---|---|
| Meaningful names | daysUntilExpiry, not d. Functions are verbs (sendInvoice), booleans read as questions (isActive, hasAccess). |
| DRY | Don't Repeat Yourself. If the same logic is copy-pasted in three places, a bug fix will be missed in one. Extract it. |
| KISS | Keep It Simple. The boring solution your teammates understand beats the clever one they don't. |
| YAGNI | You Aren't Gonna Need It. Build what the ticket needs now, not what you imagine might be needed one day. |
| Single responsibility | One function, one job. If you need "and" to describe it, split it. |
| No magic numbers | if (age >= ADULT_AGE), not if (age >= 18) scattered everywhere. |
| Early return | Handle bad cases first and exit, so the main logic isn't buried five levels deep. |
| Boy Scout rule | Leave code a little cleaner than you found it. A small rename is fine; a giant unrelated refactor belongs in its own PR. |
A small refactor, before and after
Same behaviour, much easier to read and change.
function calc(u) { if (u != null) { if (u.age >= 18) { if (u.c == "IN" || u.c == "US") { return u.p * 0.9; } } return u.p; } }
const ADULT_AGE = 18; const DISCOUNT_COUNTRIES = ["IN", "US"]; const ADULT_DISCOUNT = 0.9; function priceFor(user) { if (!user) throw new Error("user is required"); const eligible = user.age >= ADULT_AGE && DISCOUNT_COUNTRIES.includes(user.country); return eligible ? user.price * ADULT_DISCOUNT : user.price; }
What changed: a name that says what the function does, constants instead of magic numbers, full property names, an early exit for missing input (the old version silently returned undefined), and no deep nesting.
Go deeper (optional): SOLID and code smells
SOLID, in one line each
You'll hear "SOLID" in object-oriented codebases. Don't memorise it yet; just recognise it.
- Single responsibility: a class has one reason to change.
- Open/closed: add behaviour by adding code, not by editing working code.
- Liskov substitution: a subclass should work anywhere its parent works.
- Interface segregation: many small interfaces beat one giant one.
- Dependency inversion: depend on interfaces, not concrete classes, so parts are swappable and testable.
Code smells to notice
Functions longer than a screen. More than three or four parameters. Deep nesting. Copy-pasted blocks. Comments explaining confusing code that could be renamed instead. Boolean parameters like save(true, false). None are bugs, but each is a hint that code could be clearer.
- Write for the reader. Good names do most of the work.
- Simple and obvious beats clever. Build only what's needed now.
- Refactor in small, separate steps, with tests protecting you.
Testing, debugging and speed
Testing: proving your code works
An automated test is code that runs your code and checks the result. Tests let you change things confidently: if you break something, a test turns red within seconds instead of a customer finding it next week.
In plain words, a test says: "given this situation, when I call this code, then I expect this result." For example: given an adult customer in India with a price of 100, when I calculate the price, then I expect 90. If someone later breaks the discount logic, that test fails immediately and tells them exactly what broke.
Teams usually think about tests as a pyramid: many small fast tests at the bottom, a few slow realistic ones at the top.
Anatomy of a unit test: Arrange, Act, Assert
These tests cover the priceFor function from the clean code chapter. Notice one test per behaviour, including the error case.
// priceFor.test.js (Jest) describe("priceFor", () => { test("gives adults in discount countries 10% off", () => { // Arrange: set up the input const user = { age: 30, country: "IN", price: 100 }; // Act: call the code const result = priceFor(user); // Assert: check the outcome expect(result).toBe(90); }); test("charges minors full price", () => { expect(priceFor({ age: 16, country: "IN", price: 100 })).toBe(100); }); test("throws when user is missing", () => { expect(() => priceFor(null)).toThrow("user is required"); }); });
Words you'll hear
| Term | Meaning |
|---|---|
| Assertion | The check inside a test: expect(x).toBe(90). |
| Edge case | Unusual input: empty list, zero, null, huge numbers, emoji in names, leap years. |
| Mock / stub | A fake version of something slow or external (payment API, email) so tests run fast and offline. |
| Regression | Something that used to work and broke. A test added with a bug fix prevents it coming back. |
| Coverage | The percentage of lines your tests run. Useful hint, poor goal: 100% coverage can still miss bugs. |
| Flaky test | Passes sometimes, fails sometimes, with no code change. Usually timing or shared state. Report it; don't just re-run. |
| TDD | Test-Driven Development: write a failing test first, make it pass, then tidy up. "Red, green, refactor." |
- Most tests should be small, fast unit tests. Add fewer, slower integration and end-to-end tests on top.
- Test behaviour and edge cases, not just the happy path.
- Every bug fix comes with a test that would have caught it.
Debugging without panic
Debugging is a skill with a method, not a talent. When something breaks, work through these steps in order:
- Reproduce it. Find exact steps that make the bug happen every time.
- Read the whole error message. Slowly. It usually tells you what and where.
- Narrow it down. Which file, function, line? Which input triggers it?
- Form a guess, then test it. "I think
itemsis empty here." Add a log line or breakpoint to check. - Fix the cause, not the symptom. Wrapping it in try/catch to hide the error is not a fix.
- Add a test that fails without your fix and passes with it.
A request crashed. Here's the error from the logs. A stack trace lists the chain of method calls that led to the crash. Click the line where you'd start looking for the bug.
Reading Java exceptions
- Java prints the error message first, then the
at …lines. The topatline is where the exception was thrown. (Python is the opposite: the error is at the bottom.) - Skip frames from
org.springframework,java.,jdk.and other libraries. Find the first frame in your own package. - Look for
Caused by:further down. The last "Caused by" is usually the real root cause; the top exception is often just a wrapper. - Modern Java explains
NullPointerExceptions: "because the return value ofgetCity()is null" tells you exactly what was missing.
Debugger basics in IntelliJ IDEA
Start the app with the bug icon (Debug) instead of Run, then click in the left margin next to a line number to add a breakpoint.
| Action | What it does | Windows / Linux | Mac |
|---|---|---|---|
| Step over | Run this line, stop at the next one | F8 | F8 |
| Step into | Go inside the method called on this line | F7 | F7 |
| Step out | Finish this method, return to the caller | Shift+F8 | Shift+F8 |
| Resume | Run until the next breakpoint | F9 | Cmd+Opt+R |
| Evaluate expression | Run any code using the current variables | Alt+F8 | Opt+F8 |
Right-click a breakpoint to make it conditional, e.g. stop only when id == 88213. That's how you catch one bad record among thousands.
Tools that help
- Debugger with breakpoints. Pause the program on a line and inspect every variable. Learn your IDE's debugger in week one; it's the biggest time saver.
- Logging.
console.log/printis fine locally. Remove it before committing, or use the project's real logger. - Rubber duck debugging. Explain the problem out loud, line by line, to anyone or anything. You'll often spot the bug mid-sentence.
git bisect. Something worked last week and doesn't now? Bisect binary-searches your commits to find the one that broke it.- Logs for deployed code. You can't attach a debugger to production. Use the request ID and CloudWatch (the AWS chapter).
- Search the exact error message in quotes, along with the library name.
- Reproduce first. A bug you can trigger on demand is half-solved.
- Find the first stack frame in your team's code (top of a Java trace, bottom of a Python one), and read any "Caused by".
- Test one guess at a time, and finish with a regression test.
Performance, Big-O and data structures
Big-O notation describes how the work your code does grows as the input grows. It ignores exact timings and asks: if the list is ten times bigger, is the code ten times slower? A hundred times? Not slower at all?
Here's a story that happens on real teams. A developer writes a screen that compares every work order with every other work order to find duplicates. With 100 work orders in the test database, that's 10,000 comparisons: instant. A big customer has 200,000 work orders. That's 40 billion comparisons, and the screen never loads. Nothing was "wrong" with the code; it just didn't scale. Big-O is the tool for spotting that in advance.
Bar length uses a log scale (each tick is 10× more work). Times assume about 100 million simple steps per second.
| Big-O | Name | Everyday example |
|---|---|---|
O(1) | Constant | Reading arr[5], looking up a key in a hash map |
O(log n) | Logarithmic | Binary search in a sorted list, database index lookup |
O(n) | Linear | One loop over a list, list.includes(x) |
O(n log n) | Linearithmic | Good sorting algorithms (sort() in most languages) |
O(n²) | Quadratic | A loop inside a loop over the same list, e.g. comparing every pair |
Picking a data structure
| Structure | Good at | Use it when |
|---|---|---|
| Array / list | Ordered items, access by position | You need order or iterate everything |
| Hash map / dict | Find a value by key in O(1) | You look things up by ID or name |
| Set | "Have I seen this?" in O(1), no duplicates | Removing duplicates, membership checks |
| Queue | First in, first out | Processing jobs in arrival order |
| Stack | Last in, first out | Undo history, walking nested structures |
| Tree | Hierarchy, sorted data | Folders, org charts, DOM, database indexes |
for (const order of orders) { // scans every customer, every time const c = customers.find( x => x.id === order.customerId); print(c.name, order.total); }
const byId = new Map( customers.map(c => [c.id, c])); for (const order of orders) { const c = byId.get(order.customerId); print(c.name, order.total); }
Most importantly: write clear, correct code first. Optimise only when something is measurably slow, and measure again after.
- Big-O tells you how code slows down as data grows. Nested loops over big lists are the usual culprit.
- A hash map or set turns repeated searches into instant lookups.
- Correct and readable first. Measure before optimising.
APIs and the cloud
How apps, APIs and databases fit together
Most software you'll work on follows the client–server pattern. A client (browser, mobile app) asks for something. A server (backend) does the work, usually reading or writing a database, and sends back an answer. They talk through an API: an agreed list of requests the server accepts.
- API (Application Programming Interface) is a contract: "send me this, in this format, and I'll send you that back." The mobile app doesn't need to know how the server works inside; it only needs the contract.
- An endpoint is one specific address in the API, like
/api/work-orders. Each endpoint does one kind of job. - Requests and responses usually carry data as JSON (explained in the MongoDB chapter).
- HTTP is the set of rules for how requests and responses are written and sent. HTTPS is the same, encrypted so nobody in between can read it.
What a request and response look like
Every request has a method (what to do), a path (to what), headers (extra info such as who you are) and sometimes a body (the data, usually JSON). The response has a status code, headers and usually a body.
# Request: technician app → server POST /api/work-orders HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGci... # who is asking Content-Type: application/json # the body is JSON { "title": "Replace meter", "assetId": "MTR-4471", "priority": 2 } # Response: server → app HTTP/1.1 201 Created Location: /api/work-orders/88213 Content-Type: application/json { "id": 88213, "status": "OPEN", "title": "Replace meter", "assetId": "MTR-4471", "priority": 2 }
GET, POST, PUT, PATCH, DELETE
The method says what kind of action the request is. Together they cover CRUD: Create, Read, Update, Delete. Click each one to see a real request and response.
| Method | Does | Changes data? | Same result if sent twice? |
|---|---|---|---|
GET | Read | No (it's "safe") | Yes |
POST | Create something new, or trigger an action | Yes | No: twice creates two work orders |
PUT | Replace the whole resource | Yes | Yes |
PATCH | Change some fields | Yes | Usually |
DELETE | Remove | Yes | Yes (it's already gone) |
"Same result if sent twice" is called idempotent. It matters for field apps on bad mobile connections: if the app retries a request, retrying a PUT is harmless, but retrying a POST can create duplicates unless the API guards against it.
Tools for calling APIs
- Postman or Insomnia: save requests in collections and share them with the team. Ask if your team has a shared collection.
- curl in the terminal: quick, and easy to paste into a ticket.
- IntelliJ HTTP Client: write requests in a
.httpfile next to the code. - Swagger UI / OpenAPI: many Spring Boot services publish interactive API docs, often at
/swagger-ui.html.
Status codes worth memorising
The first digit tells you who to look at: 2xx success, 4xx the client sent something wrong, 5xx the server failed.
| Code | Meaning | Usually means you should… |
|---|---|---|
200 OK | Success | Celebrate quietly |
201 Created | Something new was created | Read the new ID from the response |
400 Bad Request | Input was invalid | Check the request body and validation errors |
401 Unauthorized | Not signed in, or token expired | Check the auth token |
403 Forbidden | Signed in, but not allowed | Check the user's permissions |
404 Not Found | That URL or item doesn't exist | Check the path and ID |
500 Internal Server Error | The server crashed | Read the server logs for a stack trace |
503 Service Unavailable | Server overloaded or down | Check health dashboards; retry later |
Databases in one paragraph each
- Relational (SQL), see the SQL chapter: PostgreSQL, MySQL, SQL Server. Data in tables with rows and columns, linked by IDs. Strong rules keep data consistent. The default choice for most business apps.
- NoSQL, see the MongoDB chapter: MongoDB (documents), Redis (key–value, often a cache), DynamoDB. Flexible shapes, scales easily for specific access patterns.
- Migrations: versioned scripts that change the database structure (add a column, a table). They live in the repo and run in order, so every environment's database matches the code.
- Client asks, server works, database stores. The API is the contract between them.
- 4xx means look at the request; 5xx means look at the server logs.
- Database structure changes go through migrations in the repo, never by hand in production.
AWS and reading logs in CloudWatch
AWS (Amazon Web Services) rents out servers, databases, storage and hundreds of other services, so companies don't run their own data centres. If your services run there, then when something goes wrong in dev, staging or production, their logs in CloudWatch are usually the first place you look.
AWS services you'll hear about
Most teams use a subset of these. Ask which ones, and in which accounts and regions.
- An account is a separate, walled-off area of AWS. Companies often have one account for dev, one for staging and one for production, so a mistake in dev can't touch production.
- A region is a physical location with data centres, like
us-east-1(Virginia) orap-south-1(Mumbai). Your services, and their logs, live in a specific region. - A container is your app packaged together with everything it needs to run (the right Java version, settings, libraries), so it runs the same on any machine. Docker builds containers; ECS or EKS run many of them.
| Service | What it is |
|---|---|
| EC2 | Virtual servers you rent by the hour |
| ECS / EKS | Run containers (Docker) at scale; EKS is managed Kubernetes |
| Lambda | Run a function on demand without managing a server |
| S3 | File storage ("buckets"): photos, exports, backups |
| RDS | Managed SQL databases (PostgreSQL, MySQL, Oracle…) |
| SQS / SNS | Queues and notifications that pass messages between services |
| API Gateway / Load balancer | The front door that routes requests to services |
| CloudWatch | Logs, metrics, dashboards and alarms for everything above |
| IAM / IAM Identity Center | Who can access what. How you sign in. |
Signing in
Most companies sign you in through single sign-on (IAM Identity Center, formerly AWS SSO) and an access portal, where you pick an account and role. For the command line:
# once: set up a profile (asks for your portal URL, account and role) aws configure sso # each day, or when your session expires aws sso login --profile dev # check who you are signed in as aws sts get-caller-identity --profile dev
How CloudWatch Logs is organised
- Log group: all logs for one service or function, e.g.
/ecs/workorder-serviceor/aws/lambda/send-notifications. - Log stream: one source inside the group, usually one container or server instance. A service with 4 containers has 4 streams.
- Log event: one line, with a timestamp.
In the console: open CloudWatch → Logs → Log groups, pick the group, then use Search all log streams to search every container at once.
A dispatcher reports that creating work order WO-88213 showed an error. Here are the service's logs from that minute. Search the way you would in CloudWatch: first for the work order number, then for the request ID you find next to it.
Logs Insights: querying logs like a database
CloudWatch → Logs Insights lets you query one or more log groups. Choose the time range at the top, then run queries like these:
# The 50 most recent errors fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc | limit 50 # Everything that happened in one request fields @timestamp, @message | filter @message like /req-7f3a/ | sort @timestamp asc # Errors per 5 minutes: did the problem start after a deploy? filter @message like /ERROR/ | stats count(*) as errors by bin(5m)
From the terminal
# Stream logs live, like tail -f aws logs tail /ecs/workorder-service --follow --profile dev # Only errors from the last hour aws logs tail /ecs/workorder-service --since 1h --filter-pattern ERROR --profile dev
Tips that save hours
- Seeing no logs at all? Check the account, the region (top right of the console) and the time range. These three cause most "the logs are empty" moments.
- Timestamps are usually in UTC. A problem at 3:10 PM in India (IST, UTC+5:30) appears at 09:40 UTC. The console has a UTC/local time toggle.
- Follow the request ID (also called a correlation or trace ID). It ties together every line from one request, even across services.
- Not every ERROR is your bug. Busy services always have some background errors. Look for errors that match the time and the request.
- Treat production logs as sensitive. They can contain customer names and addresses. Don't paste them into public channels; share the request ID and a short summary instead.
- Each service logs to a log group; each container writes its own stream. Search all streams at once.
- Search for the business ID, grab the request ID, then filter by it to see the whole story.
- No logs? Check account, region and time range (in UTC) before anything else.
Shipping safely
Environments, CI/CD and keeping things running
Code passes through several copies of the system, called environments, before reaching real users. Each one catches different problems.
CI/CD
In plain words: every time you push code, a robot grabs it, builds it from scratch, and runs all the tests. That's CI (Continuous Integration). If everything passes and the code is merged, another robot can deploy it, first to staging, then to production. That's CD (Continuous Delivery or Deployment). The robots never get tired or forget a step, which is why teams trust them more than manual checklists.
This guide's examples use Bitbucket Pipelines; others include GitHub Actions, GitLab CI, Jenkins and AWS CodePipeline. The pipeline is defined in a file in the repo, so you can read exactly what runs.
# bitbucket-pipelines.yml (simplified, for a Maven service) image: maven:3.9-eclipse-temurin-21 pipelines: pull-requests: '**': # every PR, whatever the branch name - step: name: Build and test caches: [maven] script: - mvn -B verify # compile, unit + integration tests branches: main: # after a PR merges - step: name: Build and test caches: [maven] script: - mvn -B verify - step: name: Deploy to staging deployment: staging script: - ./scripts/deploy.sh staging
Shipping safely
- Feature flags let you merge unfinished or risky code switched off, then turn it on for 1% of users, then everyone. Turning it off is the fastest rollback.
- Rollback means redeploying the previous version when a release goes wrong. Fix forward later, calmly.
- Semantic versioning (
MAJOR.MINOR.PATCH, e.g.2.4.1): bump PATCH for fixes, MINOR for new backwards-compatible features, MAJOR for breaking changes.
Logs, metrics and alerts
Once code is live, you can't attach a debugger. You rely on what it records.
| Log level | Use for | Example |
|---|---|---|
DEBUG | Detail useful only while developing | Cart contents: 3 items |
INFO | Normal, notable events | Order 5531 created |
WARN | Something odd, but handled | Payment API slow, retrying (attempt 2) |
ERROR | Something failed and needs attention | Payment failed for order 5531: timeout |
Metrics are numbers over time (requests per second, error rate, response time), shown on dashboards like Grafana or Datadog. Alerts page the on-call engineer when a metric crosses a threshold.
When things break: incidents
Production problems are called incidents. Healthy teams run blameless postmortems afterwards: they ask "how did our process let this happen?" not "whose fault was it?". If you cause an outage (everyone does eventually), say so quickly. Speed of reporting matters far more than the mistake.
- Code goes local → dev → staging → production. Each step catches different problems.
- CI checks every change automatically; read its config to see what runs.
- Log useful events at the right level, and report production mistakes immediately.
Security basics every engineer needs
Security isn't only the security team's job. Most real breaches start with an everyday mistake. A few habits prevent most of them.
1. Never commit secrets
Passwords, API keys, tokens and private keys go in environment variables or a secrets manager (AWS Secrets Manager, Vault), never in code. Keep a .env.example with fake values in the repo, and the real .env in .gitignore.
2. Never trust input
Anything from a user, URL, form or other system could be malicious. Validate it on the server, even if the frontend already checks it. The classic example is SQL injection:
// name = "x' OR '1'='1" // returns every user in the table db.query( "SELECT * FROM users WHERE name = '" + name + "'" );
// the driver treats name as data, // never as SQL db.query( "SELECT * FROM users WHERE name = $1", [name] );
3. More habits
- Authentication vs authorization. Authentication: who are you? Authorization: are you allowed to do this? Check both on the server for every request. Hiding a button is not security.
- Least privilege. Give users, services and API keys only the access they need.
- Escape output. Displaying user text as raw HTML enables XSS (cross-site scripting). Frameworks like React escape by default; avoid bypassing it.
- Keep dependencies updated. Most vulnerabilities live in outdated libraries. Don't ignore Dependabot or
npm auditwarnings. - Don't log sensitive data. No passwords, card numbers or full personal details in logs.
- Never store plain-text passwords. Use a proper hashing library (bcrypt, Argon2). Never write your own crypto.
The OWASP Top 10 is the standard list of the most common web security risks. Skim it once this year.
- Secrets live in environment variables, never in Git. A leaked key must be rotated.
- Validate all input on the server and use parameterised queries.
- Check permissions on the server for every request; the UI is not a security layer.
Habits and reference
Everyday habits of good engineers
Most of the job isn't clever algorithms. It's a handful of habits repeated every day. None of these need talent, only practice.
| Habit | What it looks like |
|---|---|
| Read before you write | Before changing a file, read the code around it and copy how it's done there. Consistency beats personal style. |
| Run it locally first | Get the project running on your laptop using the README. Fix the README if a step was missing; that's a great first PR. |
| Test your change | Run the app and the tests before pushing. Add a test that would have caught the bug you fixed. |
| Ship small and often | Three small PRs this week beat one giant PR next month. |
| Use the 30-minute rule | Stuck for 30 minutes with no progress? Write down what you tried and ask. |
| Own your mistakes | Broke something? Say so early and help fix it. Trust comes from honesty, not from never failing. |
| Estimate honestly | "I'm not sure, maybe 3 days, I've never touched the payment code" is a great estimate. |
| Learn the business | Understand who the users are and why features matter. It makes your technical choices better. |
| Write things down | Keep a work log of commands, gotchas and answers. It's also great material for your performance review. |
How to ask a good question
Include what you're trying to do, what you expected, what actually happened (the exact error), and what you've already tried.
hey, login isn't working, can you help?
Running the app locally, login returns 401 even with the seed user (test@shop.dev). Expected 200. I checked .env has AUTH_SECRET set and restarted the server. Error log: "invalid signature". Is there a step I'm missing?
Your first 90 days
- Weeks 1–2: get the project running, read the README and the architecture docs, ship a tiny fix (a typo, a doc update).
- Weeks 3–6: take small tickets end to end. Review others' PRs to learn the codebase.
- Weeks 7–12: take a medium feature. Join on-call shadowing if your team has it. Write down three things you'd improve.
- Match the code that already exists before inventing your own patterns.
- Asking for help early, with context, is a professional skill.
- Honesty about mistakes and estimates builds more trust than looking perfect.
Cheat sheet and glossary
| I want to… | Command |
|---|---|
| Download a project | git clone <url> |
| See what's changed and where it is | git status |
| Get the latest code from the team | git pull |
| Start a new branch | git switch -c feature/name |
| Move to another branch | git switch main |
| See exactly what I changed | git diff / git diff --staged |
| Pick changes for the next commit | git add file.js |
| Save a commit | git commit -m "Fix login bug" |
| Share my branch | git push -u origin feature/name |
| Bring main's changes into my branch | git merge main |
| See recent commits | git log --oneline |
| See who changed a line and why | git blame file.js |
| Throw away edits to one file | git restore file.js |
| Unstage a file | git restore --staged file.js |
| Fix the last commit (not yet pushed) | git commit --amend |
| Undo a pushed commit safely | git revert <hash> |
| Put work aside for a moment | git stash, later git stash pop |
| Back out of a messy merge | git merge --abort |
| Find "lost" commits | git reflog |
Beyond Git
| I want to… | Command or query |
|---|---|
| Build and test a Java service | mvn clean install |
| Run one Java test class | mvn test -Dtest=WorkOrderServiceTest |
| Start a Spring Boot app | mvn spring-boot:run |
| Create a Python virtual environment | python3 -m venv .venv |
| Activate it (Mac/Linux · Windows) | source .venv/bin/activate · .venv\Scripts\activate |
| Install a script's packages | pip install -r requirements.txt |
| See a script's options | python script.py --help |
| Sign in to AWS from the terminal | aws sso login --profile dev |
| Follow a service's logs live | aws logs tail /ecs/workorder-service --follow |
| Recent errors in Logs Insights | filter @message like /ERROR/ | sort @timestamp desc |
| Join two SQL tables | SELECT … FROM a JOIN b ON b.a_id = a.id |
| Find documents in MongoDB | db.workOrders.find({ status: "OPEN" }) |
| Call an API from the terminal | curl -H "Authorization: Bearer $TOKEN" https://api…/work-orders/88213 |
| Find my open Jira tickets (JQL) | assignee = currentUser() AND resolution = Unresolved |
Glossary · guess first, then tap
Finished? Go back to any quiz you missed. Wrong answers are where the learning happens, and there's no limit on tries.